From 02722acad139776365496203d4b0a9f5f52d6670 Mon Sep 17 00:00:00 2001 From: Teresa Pelinski Date: Fri, 9 Aug 2024 22:40:16 +0100 Subject: [PATCH] v1.0.0! added tutorial bela2python2bela, minor fixes --- docs/docs-readme.md | 6 +- pybela/Streamer.py | 23 +- pybela/Watcher.py | 2 + readme.md | 10 +- setup.py | 2 +- test/bela-test-send/render.cpp | 18 +- test/readme.md | 14 +- .../bela-code/bela2python2bela/Watcher.cpp | 1 + .../bela-code/bela2python2bela/Watcher.h | 1 + .../bela-code/bela2python2bela/render.cpp | 137 ++++++++++ tutorials/bela-code/potentiometers/sketch.js | 1 - tutorials/bela-code/timestamping/sketch.js | 1 - ...=> 1_Streamer-Bela-to-python-basics.ipynb} | 53 +--- .../2_Streamer-Bela-to-python-advanced.ipynb | 208 +++++++++++++++ .../notebooks/2_Streamer-python-to-Bela.ipynb | 64 ----- .../notebooks/3_Streamer-python-to-Bela.ipynb | 245 ++++++++++++++++++ .../{3_Monitor.ipynb => 4_Monitor.ipynb} | 13 +- .../{4_Logger.ipynb => 5_Logger.ipynb} | 4 +- .../notebooks/5_Sparse-timestamping.ipynb | 1 - tutorials/notebooks/6_Controller.ipynb | 4 +- .../notebooks/7_Sparse-timestamping.ipynb | 1 + 21 files changed, 649 insertions(+), 160 deletions(-) create mode 120000 tutorials/bela-code/bela2python2bela/Watcher.cpp create mode 120000 tutorials/bela-code/bela2python2bela/Watcher.h create mode 100644 tutorials/bela-code/bela2python2bela/render.cpp delete mode 120000 tutorials/bela-code/potentiometers/sketch.js delete mode 120000 tutorials/bela-code/timestamping/sketch.js rename tutorials/notebooks/{1_Streamer-Bela-to-python.ipynb => 1_Streamer-Bela-to-python-basics.ipynb} (90%) create mode 100644 tutorials/notebooks/2_Streamer-Bela-to-python-advanced.ipynb delete mode 100644 tutorials/notebooks/2_Streamer-python-to-Bela.ipynb create mode 100644 tutorials/notebooks/3_Streamer-python-to-Bela.ipynb rename tutorials/notebooks/{3_Monitor.ipynb => 4_Monitor.ipynb} (96%) rename tutorials/notebooks/{4_Logger.ipynb => 5_Logger.ipynb} (97%) delete mode 100644 tutorials/notebooks/5_Sparse-timestamping.ipynb create mode 100644 tutorials/notebooks/7_Sparse-timestamping.ipynb diff --git a/docs/docs-readme.md b/docs/docs-readme.md index e50f9bb..77d187b 100644 --- a/docs/docs-readme.md +++ b/docs/docs-readme.md @@ -3,7 +3,7 @@ To build the docs you will need to install `pandoc` to convert the `readme.md` i Then you can build the docs with: ```bash -rm -r docs/_build -pandoc -s readme.md -o docs/readme.rst -pipenv run sphinx-build -M html docs/ docs/_build +rm -r _build +pandoc -s ../readme.md -o readme.rst +pipenv run sphinx-build -M html . _build ``` diff --git a/pybela/Streamer.py b/pybela/Streamer.py index b9df106..69ec068 100644 --- a/pybela/Streamer.py +++ b/pybela/Streamer.py @@ -108,19 +108,6 @@ def streaming_buffers_queue(self): # returns a dict of lists instead of a dict of dequeues return {key: list(value) for key, value in self._streaming_buffers_queue.items()} - def start(self): - """Starts the websocket connection and initialises the streaming buffers queue. - """ - # self.connect() - if not self.is_connected(): - _print_warning( - f'{"Monitor" if self._mode=="MONITOR" else "Streamer" } is not connected to Bela. Run {"monitor" if self._mode=="MONITOR" else "streamer"}.connect() first.') - return 0 - self._streaming_buffers_queue = {var["name"]: deque( - maxlen=self._streaming_buffers_queue_length) for var in self.watcher_vars} - self.last_streamed_buffer = { - var["name"]: {"data": [], "timestamps": []} for var in self.watcher_vars} - @property def streaming_buffers_data(self): """Returns a dict where each key corresponds to a variable and each value to a flat list of the streamed values. Does not return timestamps of each datapoint since that depends on how often the variables are reassigned in the Bela code. @@ -143,8 +130,14 @@ def __streaming_common_routine(self, variables=[], saving_enabled=False, saving_ _print_warning("Stopping previous streaming session...") self.stop_streaming() # stop any previous streaming - if self.start() == 0: # bela is not connected - return + if not self.is_connected(): + _print_warning( + f'{"Monitor" if self._mode=="MONITOR" else "Streamer" } is not connected to Bela. Run {"monitor" if self._mode=="MONITOR" else "streamer"}.connect() first.') + return 0 + self._streaming_buffers_queue = {var["name"]: deque( + maxlen=self._streaming_buffers_queue_length) for var in self.watcher_vars} + self.last_streamed_buffer = { + var["name"]: {"data": [], "timestamps": []} for var in self.watcher_vars} if not os.path.exists(saving_dir): os.makedirs(saving_dir) diff --git a/pybela/Watcher.py b/pybela/Watcher.py index 404be90..88f9599 100644 --- a/pybela/Watcher.py +++ b/pybela/Watcher.py @@ -170,7 +170,9 @@ async def _async_stop(): await self.ws_ctrl.close() if self.ws_data is not None and self.ws_data.open: await self.ws_data.close() + if self._process_received_data_msg_worker_task is not None: self._process_received_data_msg_worker_task.cancel() + if self._sending_data_msg_worker_task is not None: self._sending_data_msg_worker_task.cancel() return asyncio.run(_async_stop()) diff --git a/readme.md b/readme.md index a74c99b..e52472e 100644 --- a/readme.md +++ b/readme.md @@ -1,10 +1,10 @@ # pybela -pybela allows interfacing with [Bela](https://bela.io/), the embedded audio platform, using Python. pybela provides a convenient way to stream, log, monitor sensor data from Bela to python. It also allows you to send buffers of data from python to Bela or control the value of variables in your Bela code from python. +pybela enables seamless interfacing with [Bela](https://bela.io/), the embedded audio platform, using python. It offers a convenient way to stream data between Bela and python in both directions. In addition to data streaming, pybela supports data logging, as well as variable monitoring and control functionalities. Below, you can find instructions to install pybela. You can find code examples at `tutorials/` and `test/`. The docs are available at [https://belaplatform.github.io/pybela/](https://belaplatform.github.io/pybela/). -pybela was developed with a machine learning use case in mind. For a complete pipeline including data acquisition, processing, model training, and deployment (including rapid cross-compilation) check the [pybela-pytorch-xc-tutorial](https://github.com/pelinski/pybela-pytorch-xc-tutorial). +pybela was developed with a machine learning use-case in mind. For a complete pipeline including data acquisition, processing, model training, and deployment (including rapid cross-compilation) check the [pybela-pytorch-xc-tutorial](https://github.com/pelinski/pybela-pytorch-xc-tutorial). ## Installation and set up @@ -85,7 +85,7 @@ scp watcher/Watcher.h watcher/Watcher.cpp root@bela.local:Bela/projects/your-pro pybela has three different modes of operation: -- **Streaming**: continuously send data from Bela to python (**NEW: or vice versa!** check the [tutorial](tutorials/notebooks/2_Streamer-python-to-Bela.ipynb)). +- **Streaming**: continuously send data from Bela to python (**NEW: and from python to Bela!** check the [tutorial](tutorials/notebooks/3_Streamer-python-to-Bela.ipynb)). - **Logging**: log data in a file in Bela and then retrieve it in python. - **Monitoring**: monitor the value of variables in the Bela code from python. - **Controlling**: control the value of variables in the Bela code from python. @@ -193,10 +193,6 @@ pipenv run python -m build --sdist # builds the .tar.gz file ## To do and known issues -**Before next release** - -- [ ] **Add** a tutorial for `.send_buffer`, and data and buffers callback - **Long term** - [ ] **Design**: remove nest_asyncio? diff --git a/setup.py b/setup.py index e57824c..3525073 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="pybela", - version="0.1.0", + version="1.0.0", author="Teresa Pelinski", author_email="teresapelinski@gmail.com", description="pybela allows interfacing with Bela, the embedded audio platform, using Python. pybela provides a convenient way to stream, log, and monitor sensor data from your Bela device to your laptop, or alternatively, to stream values to a Bela program from your laptop.", diff --git a/test/bela-test-send/render.cpp b/test/bela-test-send/render.cpp index 06461e9..65ba8e1 100644 --- a/test/bela-test-send/render.cpp +++ b/test/bela-test-send/render.cpp @@ -18,12 +18,6 @@ ReceivedBuffer receivedBuffer; uint receivedBufferHeaderSize; uint64_t totalReceivedCount; -struct CallbackBuffer { - uint32_t guiBufferId; - std::vector bufferData; - uint64_t count; -}; -CallbackBuffer callbackBuffers[2]; bool binaryDataCallback(const std::string& addr, const WSServerDetails* id, const unsigned char* data, size_t size, void* arg) { @@ -39,10 +33,9 @@ bool binaryDataCallback(const std::string& addr, const WSServerDetails* id, cons Bela_getDefaultWatcherManager()->tick(totalReceivedCount); int _id = receivedBuffer.bufferId; if (_id >= 0 && _id < myVars.size()) { - callbackBuffers[_id].bufferData = receivedBuffer.bufferData; - callbackBuffers[_id].count++; - for (size_t i = 0; i < callbackBuffers[_id].bufferData.size(); ++i) { - *myVars[_id] = callbackBuffers[_id].bufferData[i]; + + for (size_t i = 0; i < receivedBuffer.bufferData.size(); ++i) { + *myVars[_id] = receivedBuffer.bufferData[i]; } } @@ -55,12 +48,9 @@ bool setup(BelaContext* context, void* userData) { Bela_getDefaultWatcherManager()->setup(context->audioSampleRate); // set sample rate in watcher for (int i = 0; i < 2; ++i) { - callbackBuffers[i].guiBufferId = Bela_getDefaultWatcherManager()->getGui().setBuffer('f', 1024); - callbackBuffers[i].count = 0; + Bela_getDefaultWatcherManager()->getGui().setBuffer('f', 1024); } - printf("dataBufferId_1: %d, dataBufferId_2: %d \n", callbackBuffers[0].guiBufferId, callbackBuffers[1].guiBufferId); - Bela_getDefaultWatcherManager()->getGui().setBinaryDataCallback(binaryDataCallback); receivedBufferHeaderSize = sizeof(receivedBuffer.bufferId) + sizeof(receivedBuffer.bufferType) + sizeof(receivedBuffer.bufferLen) + sizeof(receivedBuffer.empty); diff --git a/test/readme.md b/test/readme.md index a03c5c7..d443dab 100644 --- a/test/readme.md +++ b/test/readme.md @@ -7,7 +7,7 @@ The watcher code is already included in `bela-test`. You can update your Bela AP To run the tests, copy the `bela-test` code into your Bela, add the `Watcher`` library compile and run it: ```bash -rsync -rvL test/bela-test root@bela.local:Bela/projects/ +rsync -rvL test/bela-test test/bela-test-send root@bela.local:Bela/projects/ ssh root@bela.local "make -C Bela stop Bela PROJECT=bela-test run" ``` @@ -16,3 +16,15 @@ Once the `bela-test` project is running on Bela, you can run the python tests by ```bash python test.py # or `pipenv run python test.py` if you are using a pipenv environment ``` + +You can also test the `bela-test-send` project by running: + +```bash +ssh root@bela.local "make -C Bela stop Bela PROJECT=bela-test run" +``` +and then running the python tests with: + +```bash +python test-send.py # or `pipenv run python test-send.py` if you are using a pipenv environment +``` + \ No newline at end of file diff --git a/tutorials/bela-code/bela2python2bela/Watcher.cpp b/tutorials/bela-code/bela2python2bela/Watcher.cpp new file mode 120000 index 0000000..9477c2f --- /dev/null +++ b/tutorials/bela-code/bela2python2bela/Watcher.cpp @@ -0,0 +1 @@ +../../../watcher/Watcher.cpp \ No newline at end of file diff --git a/tutorials/bela-code/bela2python2bela/Watcher.h b/tutorials/bela-code/bela2python2bela/Watcher.h new file mode 120000 index 0000000..059dbc5 --- /dev/null +++ b/tutorials/bela-code/bela2python2bela/Watcher.h @@ -0,0 +1 @@ +../../../watcher/Watcher.h \ No newline at end of file diff --git a/tutorials/bela-code/bela2python2bela/render.cpp b/tutorials/bela-code/bela2python2bela/render.cpp new file mode 100644 index 0000000..fc8a02d --- /dev/null +++ b/tutorials/bela-code/bela2python2bela/render.cpp @@ -0,0 +1,137 @@ +#include +#include +#include +#include +#include + +#define NUM_OUTPUTS 2 +#define MAX_EXPECTED_BUFFER_SIZE 1024 + +Watcher pot1("pot1"); +Watcher pot2("pot2"); + +uint gPot1Ch = 0; +uint gPot2Ch = 1; + +std::vector> circularBuffers(NUM_OUTPUTS); + +size_t circularBufferSize = 30 * 1024; +size_t prefillSize = 2.5 * 1024; +uint32_t circularBufferWriteIndex[NUM_OUTPUTS] = {0}; +uint32_t circularBufferReadIndex[NUM_OUTPUTS] = {0}; + +struct ReceivedBuffer { + uint32_t bufferId; + char bufferType[4]; + uint32_t bufferLen; + uint32_t empty; + std::vector bufferData; +}; +ReceivedBuffer receivedBuffer; +uint receivedBufferHeaderSize; +uint64_t totalReceivedCount; // total number of received buffers + +unsigned int gAudioFramesPerAnalogFrame; +float gInvAudioFramesPerAnalogFrame; +float gInverseSampleRate; +float gPhase1; +float gPhase2; +float gFrequency1 = 440.0f; +float gFrequency2 = 880.0f; + +// this callback is called every time a buffer is received from python. it parses the received data into the ReceivedBuffer struct, and then writes the data to the circular buffer which is read in the +// render function +bool binaryDataCallback(const std::string& addr, const WSServerDetails* id, const unsigned char* data, size_t size, void* arg) { + + if (totalReceivedCount == 0) { + RtThread::setThisThreadPriority(1); + } + + totalReceivedCount++; + + // parse buffer header + std::memcpy(&receivedBuffer, data, receivedBufferHeaderSize); + receivedBuffer.bufferData.resize(receivedBuffer.bufferLen); + // parse buffer data + std::memcpy(receivedBuffer.bufferData.data(), data + receivedBufferHeaderSize, receivedBuffer.bufferLen * sizeof(float)); + + // write the data onto the circular buffer + int _id = receivedBuffer.bufferId; + if (_id >= 0 && _id < NUM_OUTPUTS) { + for (size_t i = 0; i < receivedBuffer.bufferLen; ++i) { + circularBuffers[_id][circularBufferWriteIndex[_id]] = receivedBuffer.bufferData[i]; + circularBufferWriteIndex[_id] = (circularBufferWriteIndex[_id] + 1) % circularBufferSize; + } + } + + return true; +} + +bool setup(BelaContext* context, void* userData) { + + Bela_getDefaultWatcherManager()->getGui().setup(context->projectName); + Bela_getDefaultWatcherManager()->setup(context->audioSampleRate); // set sample rate in watcher + + gAudioFramesPerAnalogFrame = context->audioFrames / context->analogFrames; + gInvAudioFramesPerAnalogFrame = 1.0 / gAudioFramesPerAnalogFrame; + gInverseSampleRate = 1.0 / context->audioSampleRate; + + // initialize the Gui buffers and circular buffers + for (int i = 0; i < NUM_OUTPUTS; ++i) { + Bela_getDefaultWatcherManager()->getGui().setBuffer('f', MAX_EXPECTED_BUFFER_SIZE); + circularBuffers[i].resize(circularBufferSize, 0.0f); + // the write index is given some "advantage" (prefillSize) so that the read pointer does not catch up the write pointer + circularBufferWriteIndex[i] = prefillSize % circularBufferSize; + } + + Bela_getDefaultWatcherManager()->getGui().setBinaryDataCallback(binaryDataCallback); + + // vars and preparation for parsing the received buffer + receivedBufferHeaderSize = sizeof(receivedBuffer.bufferId) + sizeof(receivedBuffer.bufferType) + sizeof(receivedBuffer.bufferLen) + sizeof(receivedBuffer.empty); + totalReceivedCount = 0; + receivedBuffer.bufferData.reserve(MAX_EXPECTED_BUFFER_SIZE); + + return true; +} + +void render(BelaContext* context, void* userData) { + for (unsigned int n = 0; n < context->audioFrames; n++) { + uint64_t frames = context->audioFramesElapsed + n; + + if (gAudioFramesPerAnalogFrame && !(n % gAudioFramesPerAnalogFrame)) { + Bela_getDefaultWatcherManager()->tick(frames * gInvAudioFramesPerAnalogFrame); // watcher timestamps + + // read sensor values and put them in the watcher + pot1 = analogRead(context, n / gAudioFramesPerAnalogFrame, gPot1Ch); + pot2 = analogRead(context, n / gAudioFramesPerAnalogFrame, gPot2Ch); + + // read the values sent from python (they're in the circular buffer) + for (unsigned int i = 0; i < NUM_OUTPUTS; i++) { + + if (totalReceivedCount > 0 && (circularBufferReadIndex[i] + 1) % circularBufferSize != circularBufferWriteIndex[i]) { + circularBufferReadIndex[i] = (circularBufferReadIndex[i] + 1) % circularBufferSize; + } else if (totalReceivedCount > 0) { + rt_printf("The read pointer has caught the write pointer up in buffer %d – try increasing prefillSize\n", i); + } + } + } + float amp1 = circularBuffers[0][circularBufferReadIndex[0]]; + float amp2 = circularBuffers[1][circularBufferReadIndex[1]]; + + float out = amp1 * sinf(gPhase1) + amp2 * sinf(gPhase2); + + for (unsigned int channel = 0; channel < context->audioOutChannels; channel++) { + audioWrite(context, n, channel, out); + } + + gPhase1 += 2.0f * (float)M_PI * gFrequency1 * gInverseSampleRate; + if (gPhase1 > M_PI) + gPhase1 -= 2.0f * (float)M_PI; + gPhase2 += 2.0f * (float)M_PI * gFrequency2 * gInverseSampleRate; + if (gPhase2 > M_PI) + gPhase2 -= 2.0f * (float)M_PI; + } +} + +void cleanup(BelaContext* context, void* userData) { +} \ No newline at end of file diff --git a/tutorials/bela-code/potentiometers/sketch.js b/tutorials/bela-code/potentiometers/sketch.js deleted file mode 120000 index 0e717d7..0000000 --- a/tutorials/bela-code/potentiometers/sketch.js +++ /dev/null @@ -1 +0,0 @@ -../../../watcher/sketch.js \ No newline at end of file diff --git a/tutorials/bela-code/timestamping/sketch.js b/tutorials/bela-code/timestamping/sketch.js deleted file mode 120000 index 0e717d7..0000000 --- a/tutorials/bela-code/timestamping/sketch.js +++ /dev/null @@ -1 +0,0 @@ -../../../watcher/sketch.js \ No newline at end of file diff --git a/tutorials/notebooks/1_Streamer-Bela-to-python.ipynb b/tutorials/notebooks/1_Streamer-Bela-to-python-basics.ipynb similarity index 90% rename from tutorials/notebooks/1_Streamer-Bela-to-python.ipynb rename to tutorials/notebooks/1_Streamer-Bela-to-python-basics.ipynb index 6f24776..09ac92c 100644 --- a/tutorials/notebooks/1_Streamer-Bela-to-python.ipynb +++ b/tutorials/notebooks/1_Streamer-Bela-to-python-basics.ipynb @@ -4,11 +4,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# pybela Tutorial 1: Streamer – Bela to python\n", - "This notebook is a tutorial for the Streamer class in the pybela python library. You can use the Streamer to stream data from Bela to python or viceversa. \n", + "# pybela Tutorial 1: Streamer – Bela to python basics\n", + "This notebook is a tutorial for the Streamer class in the pybela python library. You can use the Streamer to stream data from Bela to python or vice versa. \n", "\n", "In this tutorial we will be looking at sending data from Bela to python. The Streamer allows you to start and stop streaming, to stream a given number of data points, to plot the data as it arrives, and to save and load the streamed data into `.txt` files. \n", "\n", + "The complete documentation for the pybela library can be found in [https://belaplatform.github.io/pybela/](https://belaplatform.github.io/pybela/).\n", + "\n", "To run this tutorial, first copy the `bela-code/potentiometers` project onto Bela. If your Bela is connected to your laptop, you can run the cell below:" ] }, @@ -37,7 +39,9 @@ "metadata": {}, "source": [ "### Setting up the circuit\n", - "The Bela code expects two potentiometers connected to analog inputs 0 and 1. Potentiometers have 3 pins. To connect a potentiometer to Bela, attach the left pin to the Bela 3.3V pin, the central pin to the desired analog input (e.g. 0) and the right pin to the Bela GND pin:\n", + "In this example we will be using two potentiometers as our analog signals, but you can connect whichever sensors you like to analog channels 0 and 1.\n", + "\n", + "Potentiometers have 3 pins. To connect a potentiometer to Bela, attach the left pin to the Bela 3.3V pin, the central pin to the desired analog input (e.g. 0) and the right pin to the Bela GND pin:\n", "\n", "

\n", "\n", @@ -89,7 +93,10 @@ "source": [ "import asyncio\n", "import pandas as pd\n", - "from pybela import Streamer" + "from pybela import Streamer\n", + "import os\n", + "# os.environ['BOKEH_ALLOW_WS_ORIGIN'] = \"1t4j54lsdj67h02ol8hionopt4k7b7ngd9483l5q5pagr3j2droq\" # uncomment if running on vscode\n", + "os.environ['BOKEH_ALLOW_WS_ORIGIN'] = \"localhost:8888\" # uncomment if running on jupyter" ] }, { @@ -265,7 +272,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "More advanced timestamping methods will be shown in the tutorial notebook `4_Sparse_timestamping.ipynb`\n", + "More advanced timestamping methods will be shown in the tutorial notebook `7_Sparse_timestamping.ipynb`\n", "\n", "There is a limited amount of data that is stored in the streamer. This quantity can be modified by changing the buffer queue length. The streamer receives the data in buffers of fixed length that get stored in a queue that also has a fixed length. You can calculate the maximum amount of data the streamer can store for each variable:\n", "\n", @@ -300,42 +307,6 @@ "streamer.streaming_buffers_queue_length = 10" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Streaming a fixed number of values\n", - "Alternatively, you can stream a fixed number of values of a variable using `stream_n_values()`. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "n_values = 1000\n", - "streaming_buffer = streamer.stream_n_values(\n", - " variables=[var[\"name\"] for var in streamer.watcher_vars], n_values=n_values)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Since the data buffers received from Bela have a fixed size, unless the number of values `n_values` is a multiple of the data buffers size, the streamer will always return a few more values than asked for." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "for var in streamer.watcher_vars:\n", - " print(f'Variable: {var[\"name\"]}, buffer length: {var[\"data_length\"]}, number of streamed values: {len(streamer.streaming_buffers_data[var[\"name\"]])}')" - ] - }, { "cell_type": "markdown", "metadata": {}, diff --git a/tutorials/notebooks/2_Streamer-Bela-to-python-advanced.ipynb b/tutorials/notebooks/2_Streamer-Bela-to-python-advanced.ipynb new file mode 100644 index 0000000..342c708 --- /dev/null +++ b/tutorials/notebooks/2_Streamer-Bela-to-python-advanced.ipynb @@ -0,0 +1,208 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# pybela Tutorial 2: Streamer – Bela to python advanced\n", + "This notebook is a tutorial for the Streamer class in the pybela python library. You can use the Streamer to stream data from Bela to python or vice versa. \n", + "\n", + "In this tutorial we will be looking at more advanced features to send data from Bela to python. \n", + "\n", + "The complete documentation for the pybela library can be found in [https://belaplatform.github.io/pybela/](https://belaplatform.github.io/pybela/).\n", + "\n", + "If you didn't do it in the previous tutorial, copy the `bela-code/python-to-bela` project onto Bela. If your Bela is connected to your laptop, you can run the cell below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!rsync -rvL ../bela-code/python-to-bela root@bela.local:Bela/projects" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then you can compile and run the project using either the IDE or by running the following command in the Terminal:\n", + "```bash\n", + "ssh root@bela.local \"make -C Bela stop Bela PROJECT=python-to-bela run\" \n", + "```\n", + "(Running this on a jupyter notebook will block the cell until the program is stopped on Bela.) You will also need to connect two potentiometers to Bela analog inputs 0 and 1. Instructions on how to do so and some details on the Bela code are given in the notebook `1_Streamer-Bela-to-python-basics.ipynb`.\n", + "\n", + "First, we need to import the pybela library, create a Streamer object and connect to Bela." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import asyncio\n", + "from pybela import Streamer\n", + "\n", + "streamer = Streamer()\n", + "streamer.connect()\n", + "\n", + "variables = [\"pot1\", \"pot2\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Streaming a fixed number of values\n", + "You can can use the method `stream_n_values` to stream a fixed number of values of a variable. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "n_values = 1000\n", + "streaming_buffer = streamer.stream_n_values(\n", + " variables=[var[\"name\"] for var in variables], n_values=n_values)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since the data buffers received from Bela have a fixed size, unless the number of values `n_values` is a multiple of the data buffers size, the streamer will always return a few more values than asked for." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for var in variables:\n", + " print(f'Variable: {var[\"name\"]}, buffer length: {var[\"data_length\"]}, number of streamed values: {len(streamer.streaming_buffers_data[var[\"name\"]])}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Scheduling streaming sessions\n", + "You can schedule a streaming session to start and stop at a specific time using the `schedule_streaming()` method. This method takes the same arguments as `start_streaming()`, but it also takes a `timestamps` and `durations` argument." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "latest_timestamp = streamer.get_latest_timestamp() # get the latest timestamp\n", + "sample_rate = streamer.sample_rate # get the sample rate\n", + "start_timestamp = latest_timestamp + sample_rate # start streaming 1 second after the latest timestamp\n", + "duration = sample_rate # stream for 2 seconds\n", + "\n", + "streamer.schedule_streaming(\n", + " variables=variables,\n", + " timestamps=[start_timestamp, start_timestamp],\n", + " durations=[duration, duration],\n", + " saving_enabled=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### On-buffer and on-block callbacks\n", + "Up until now, we have been streaming data for a period of time and processed the data once the streaming has finished. However, you can also process the data as it is being received. You can do this by passing a callback function to the `on_buffer` or `on_block` arguments of the `start_streaming()` method. \n", + "\n", + "The `on_buffer` callback will be called every time a buffer is received from Bela. We will need to define a callback function that takes one argument, the buffer. The Streamer will call that function every time it receives a buffer. You can also pass variables to the callback function by using the `callback_args` argument of the `start_streaming()` method. Let's see an example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timestamps = {var: [] for var in variables}\n", + "buffers = {var: [] for var in variables}\n", + "\n", + "def callback(buffer, timestamps, buffers):\n", + " print(\"Buffer received\")\n", + " \n", + " _var = buffer[\"name\"]\n", + " timestamps[_var].append(\n", + " buffer[\"buffer\"][\"ref_timestamp\"])\n", + " buffers[_var].append(buffer[\"buffer\"][\"data\"])\n", + " \n", + " print(_var, timestamps[_var][-1])\n", + "\n", + "streamer.start_streaming(\n", + " variables, saving_enabled=False, on_buffer_callback=callback, callback_args=(timestamps, buffers))\n", + "\n", + "await asyncio.sleep(2)\n", + "streamer.stop_streaming()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's now look at the `on_block`callback. We call block to a group of buffers. If you are streaming two variables, `pot1` and `pot2`, a block of buffers will contain a buffer for `pot1` and a buffer for `pot2`. If `pot1` and `pot2` have the same buffer size and they are being streamed at the same rate, `pot1` and `pot2` will be aligned in time. This is useful if you are streaming multiple variables and you want to process them together. \n", + "\n", + "The `on_block` callback will be called every time a block of buffers is received from Bela. We will need to define a callback function that takes one argument, the block. The Streamer will call that function every time it receives a block of buffers. Let's see an example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "timestamps = {var: [] for var in variables}\n", + "buffers = {var: [] for var in variables}\n", + "\n", + "def callback(block, timestamps, buffers):\n", + " print(\"Block received\")\n", + " \n", + " for buffer in block:\n", + " var = buffer[\"name\"]\n", + " timestamps[var].append(buffer[\"buffer\"][\"ref_timestamp\"])\n", + " buffers[var].append(buffer[\"buffer\"][\"data\"])\n", + "\n", + " print(var, timestamps[var][-1])\n", + " \n", + "streamer.start_streaming(\n", + " variables, saving_enabled=False, on_block_callback=callback, callback_args=(timestamps, buffers))\n", + "await asyncio.sleep(2)\n", + "streamer.stop_streaming()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "pybela-2uXYSGIe", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.16" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tutorials/notebooks/2_Streamer-python-to-Bela.ipynb b/tutorials/notebooks/2_Streamer-python-to-Bela.ipynb deleted file mode 100644 index ff63df0..0000000 --- a/tutorials/notebooks/2_Streamer-python-to-Bela.ipynb +++ /dev/null @@ -1,64 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# pybela Tutorial 2: Streamer – python to Bela\n", - "This notebook is a tutorial for the Streamer class in the pybela python library. You can use the Streamer to stream data from Bela to python or viceversa. \n", - "\n", - "In this tutorial we will be looking at sending data from python to Bela. In this case, the routine is quite simple as the only available functionality is sending a buffer of a certain type and size (the `streamer.send_buffer()` method).\n", - "\n", - "To run this tutorial, first copy the `bela-code/python-to-bela` project onto Bela. If your Bela is connected to your laptop, you can run the cell below:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!rsync -rvL ../bela-code/python-to-bela root@bela.local:Bela/projects" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Then you can compile and run the project using either the IDE or by running the following command in the Terminal:\n", - "```bash\n", - "ssh root@bela.local \"make -C Bela stop Bela PROJECT=python-to-bela run\" \n", - "```\n", - "(Running this on a jupyter notebook will block the cell until the program is stopped on Bela.)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "..." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.16" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/tutorials/notebooks/3_Streamer-python-to-Bela.ipynb b/tutorials/notebooks/3_Streamer-python-to-Bela.ipynb new file mode 100644 index 0000000..387622e --- /dev/null +++ b/tutorials/notebooks/3_Streamer-python-to-Bela.ipynb @@ -0,0 +1,245 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# pybela Tutorial 3: Streamer – python to Bela\n", + "This notebook is a tutorial for the Streamer class in the pybela python library. You can use the Streamer to stream data from Bela to python or viceversa. The complete documentation for the pybela library can be found in [https://belaplatform.github.io/pybela/](https://belaplatform.github.io/pybela/).\n", + "\n", + "In this tutorial we will be looking at sending data from python to Bela. There is only one method available in the Streamer class for this purpose: `send_buffer()`. This method sends a buffer of a certain type and size to Bela. \n", + "\n", + "To run this tutorial, first copy the `bela-code/bela2python2bela` project onto Bela. If your Bela is connected to your laptop, you can run the cell below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!rsync -rvL ../bela-code/bela2python2bela root@bela.local:Bela/projects" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then you can compile and run the project using either the IDE or by running the following command in the Terminal:\n", + "```bash\n", + "ssh root@bela.local \"make -C Bela stop Bela PROJECT=bela2python2bela run\" \n", + "```\n", + "(Running this on a jupyter notebook will block the cell until the program is stopped on Bela.) \n", + "\n", + "This program expects two analog signals in channels 0 and 1, you can keep using the potentiometer setup from the previous tutorials (check the schematic in `1_Streamer-Bela-to-python.ipynb`)\n", + "\n", + "In this example we will be sending the values of the two potentiometers from Bela to python. Once received in python, we will send them immediately back to Bela. The values received in Bela will be used to modulate the amplitude of two sine waves. It is admittedly an overly complicated way to modulate two sine waves in Bela, as you could of course use the potentiometer values directly, without having to send them to python and back. However, this example can serve as a template for more complex applications where you can process the data in python before sending it back to Bela. \n", + "\n", + "## Understanding the Bela code\n", + "If you are not familiar with auxiliary tasks and circular buffers, we recommend you follow first [Lesson 11](https://youtu.be/xQBftd7WNY8?si=ns6ojYnfQ_GVtCQI) and [Lesson 17](https://youtu.be/2uyWn8P0CVg?si=Ymy-NN_HKS-Q3xL0) of the C++ Real-Time Audio Programming with Bela course. \n", + "\n", + "Let's first take a look at the Bela code. The `setup()` function initializes the Bela program and some necessary variables. First, we set up the Watcher with the `Bela_getDefaultWatcherManager()` function. We then calculate the inverse of some useful variables (multiplying by the inverse is faster than dividing, so we precompute the inverse in `setup` and use it later in `render`). We then initialize the GUI buffers (these are the internal buffers Bela uses to receive the data) and the `circularBuffers`. The `circularBuffers` are used to store the parsed data from the GUI buffers, and are the variables we will use in `render` to access the data we have sent from python. We also set up the `binaryDataCallback` function, which will be called when Bela receives a buffer from python. \n", + "\n", + "\n", + "```cpp\n", + "bool setup(BelaContext* context, void* userData) {\n", + "\n", + " Bela_getDefaultWatcherManager()->getGui().setup(context->projectName);\n", + " Bela_getDefaultWatcherManager()->setup(context->audioSampleRate); // set sample rate in watcher\n", + "\n", + " gAudioFramesPerAnalogFrame = context->audioFrames / context->analogFrames;\n", + " gInvAudioFramesPerAnalogFrame = 1.0 / gAudioFramesPerAnalogFrame;\n", + " gInverseSampleRate = 1.0 / context->audioSampleRate;\n", + "\n", + " // initialize the Gui buffers and circular buffers\n", + " for (int i = 0; i < NUM_OUTPUTS; ++i) {\n", + " Bela_getDefaultWatcherManager()->getGui().setBuffer('f', MAX_EXPECTED_BUFFER_SIZE);\n", + " circularBuffers[i].resize(circularBufferSize, 0.0f);\n", + " // the write index is given some \"advantage\" (prefillSize) so that the read pointer does not catch up the write pointer\n", + " circularBufferWriteIndex[i] = prefillSize % circularBufferSize;\n", + " }\n", + "\n", + " Bela_getDefaultWatcherManager()->getGui().setBinaryDataCallback(binaryDataCallback);\n", + "\n", + " // vars and preparation for parsing the received buffer\n", + " receivedBufferHeaderSize = sizeof(receivedBuffer.bufferId) + sizeof(receivedBuffer.bufferType) + sizeof(receivedBuffer.bufferLen) + sizeof(receivedBuffer.empty);\n", + " totalReceivedCount = 0;\n", + " receivedBuffer.bufferData.reserve(MAX_EXPECTED_BUFFER_SIZE);\n", + "\n", + " return true;\n", + "}\n", + "```\n", + "\n", + "Let's now take a look at the `render()` function. The render function is called once per audio block, so inside of it we iterate over the audio blocks. Since the potentiometers are analog signals, and in Bela the analog inputs are typically sampled at a lower rate than the audio, we read the potentiometers once every 2 audio frames (in the code, `gAudioFramesPerAnalogFrame` is equal to 2 if you are using the default 8 audio channels). Since the variables `pot1` and `pot2` are in the Watcher, these will be streamed to python if we run `start_streaming()` in python.\n", + "\n", + "Next, we check if the variable `totalReceivedCount` is greater than 0, which means that we have received at least a buffer from python. If we have received buffers and the read pointer has not caught up with the write pointer, we advance the read pointer in the circular buffer. The reason why we check if we have received a buffer first, is because we don't want to advance the read pointer if we haven't received any data yet, as then the read pointer would catch up with the write pointer. \n", + "\n", + "Finally, we read the values from the circular buffer and use them to modulate the amplitude of two sine waves. We then write the output to the audio channels.\n", + "\n", + "\n", + "\n", + "```cpp\n", + "\n", + "void render(BelaContext* context, void* userData) {\n", + " for (unsigned int n = 0; n < context->audioFrames; n++) {\n", + " uint64_t frames = context->audioFramesElapsed + n;\n", + "\n", + " if (gAudioFramesPerAnalogFrame && !(n % gAudioFramesPerAnalogFrame)) {\n", + " Bela_getDefaultWatcherManager()->tick(frames * gInvAudioFramesPerAnalogFrame); // watcher timestamps\n", + "\n", + " // read sensor values and put them in the watcher\n", + " pot1 = analogRead(context, n / gAudioFramesPerAnalogFrame, gPot1Ch);\n", + " pot2 = analogRead(context, n / gAudioFramesPerAnalogFrame, gPot2Ch);\n", + "\n", + " // read the values sent from python (they're in the circular buffer)\n", + " for (unsigned int i = 0; i < NUM_OUTPUTS; i++) {\n", + "\n", + " if (totalReceivedCount > 0 && (circularBufferReadIndex[i] + 1) % circularBufferSize != circularBufferWriteIndex[i]) {\n", + " circularBufferReadIndex[i] = (circularBufferReadIndex[i] + 1) % circularBufferSize;\n", + " } else if (totalReceivedCount > 0) {\n", + " rt_printf(\"The read pointer has caught the write pointer up in buffer %d – try increasing prefillSize\\n\", i);\n", + " }\n", + " }\n", + " }\n", + "\n", + " float amp1 = circularBuffers[0][circularBufferReadIndex[0]];\n", + " float amp2 = circularBuffers[1][circularBufferReadIndex[1]];\n", + "\n", + " float out = amp1 * sinf(gPhase1) + amp2 * sinf(gPhase2);\n", + "\n", + " for (unsigned int channel = 0; channel < context->audioOutChannels; channel++) {\n", + " audioWrite(context, n, channel, out);\n", + " }\n", + "\n", + " gPhase1 += 2.0f * (float)M_PI * gFrequency1 * gInverseSampleRate;\n", + " if (gPhase1 > M_PI)\n", + " gPhase1 -= 2.0f * (float)M_PI;\n", + " gPhase2 += 2.0f * (float)M_PI * gFrequency2 * gInverseSampleRate;\n", + " if (gPhase2 > M_PI)\n", + " gPhase2 -= 2.0f * (float)M_PI;\n", + "\n", + " }\n", + "}\n", + "```\n", + "\n", + "Let's now run the python code:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pybela import Streamer\n", + "streamer = Streamer()\n", + "streamer.connect()\n", + "\n", + "variables = [\"pot1\", \"pot2\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `send_buffer` function takes 4 arguments: the buffer id, the type of the data that goes in the buffer, the buffer length and the buffer data. Since we will be sending back the buffers we receive from Bela, we can get the type and length of the buffer through the streamer:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "buffer_type = streamer.get_prop_of_var(\"pot1\", \"type\")\n", + "buffer_length = streamer.get_prop_of_var(\"pot1\", \"data_length\")\n", + "\n", + "buffer_type, buffer_length\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we will be using the `block_callback` instead of the `buffer_callback`, as the `block` callback is more efficient. It should be noted that we are receiving and sending blocks of data every 1024/22050 = 0.05 seconds, and the maximum latency is given by the `prefillSize` variable in the Bela code (which is set to 2.5*1024/22050 = 0.12 seconds), so using functions is crucial to meet the real-time deadlines." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def callback(block):\n", + " \n", + " for buffer in block:\n", + " \n", + " _var = buffer[\"name\"]\n", + " timestamp = buffer[\"buffer\"][\"ref_timestamp\"]\n", + " data = buffer[\"buffer\"][\"data\"]\n", + " \n", + " buffer_id = 0 if _var == \"pot1\" else 1\n", + "\n", + " print(buffer_id, timestamp)\n", + " # do some data processing here...\n", + " processed_data = data\n", + " \n", + " # send processed_data back\n", + " streamer.send_buffer(buffer_id, buffer_type,\n", + " buffer_length, processed_data)\n", + "\n", + "streamer.start_streaming(\n", + " variables, saving_enabled=False, on_block_callback=callback)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you plug in your headphones to the audio output of Bela, you should hear two sine waves modulated by the potentiometers. The modulation (the amplitude change) is given by the value sent by python, not the analog input directly on Bela. As mentioned before, this is an overly complicated way to modulate two sine waves, but it can serve as a template for more complex applications where you can process the data in python before sending it back to Bela." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "streamer.stop_streaming()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.16" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/tutorials/notebooks/3_Monitor.ipynb b/tutorials/notebooks/4_Monitor.ipynb similarity index 96% rename from tutorials/notebooks/3_Monitor.ipynb rename to tutorials/notebooks/4_Monitor.ipynb index d32b366..5602d99 100644 --- a/tutorials/notebooks/3_Monitor.ipynb +++ b/tutorials/notebooks/4_Monitor.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# pybela tutorial 3: Monitor\n", + "# pybela tutorial 4: Monitor\n", "This tutorial expects the `potentiometers` project to be running on Bela. If the Bela is connected to your laptop, you can run the cell below to copy the `potentiometers` code with the `Watcher` library onto your Bela:" ] }, @@ -25,14 +25,9 @@ "```bash\n", "ssh root@bela.local \"make -C Bela stop Bela PROJECT=potentiometers run\" \n", "```\n", - "(Running this on a jupyter notebook will block the cell until the program is stopped on Bela.)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You will also need to connect two potentiometers to Bela analog inputs 0 and 1. Instructions on how to do so and some details on the Bela code are given in the notebook `1_Streamer.ipynb`.\n", + "(Running this on a jupyter notebook will block the cell until the program is stopped on Bela.)\n", + "\n", + "You will also need to connect two potentiometers to Bela analog inputs 0 and 1. Instructions on how to do so and some details on the Bela code are given in the notebook `1_Streamer-Bela-to-python-basics.ipynb`. The complete documentation for the pybela library can be found in [https://belaplatform.github.io/pybela/](https://belaplatform.github.io/pybela/).\n", "\n", "This notebook is a tutorial for the Monitor class in the pybela python library. The monitor allows you to \"take a look\" at variables in your Bela code. By taking a look we mean either requesting a single value (*what value does `pot1` have right now?*) or sampling the value of a variable, that is, getting a value every number of frames (*can you tell me the value of `pot1` every 1000 frames?*). The monitor can be useful to calibrate sensors or, in general, debug your Bela code. \n", "\n", diff --git a/tutorials/notebooks/4_Logger.ipynb b/tutorials/notebooks/5_Logger.ipynb similarity index 97% rename from tutorials/notebooks/4_Logger.ipynb rename to tutorials/notebooks/5_Logger.ipynb index 9cb55a0..853ad24 100644 --- a/tutorials/notebooks/4_Logger.ipynb +++ b/tutorials/notebooks/5_Logger.ipynb @@ -4,9 +4,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# pybela Tutorial 3: Logger\n", + "# pybela Tutorial 5: Logger\n", "This notebook is a tutorial for the Logger class in the pybela python library. As opposed to the Streamer, the Logger stores variable values directly in binary files in the Bela board. This is more reliable than streaming data with the Streamer with the saving mode enabled, which depends on the websocket connection. The Logger will store the data in Bela even if the websocket connection is lost, and you can retrieve the data later. \n", "\n", + "The complete documentation for the pybela library can be found in [https://belaplatform.github.io/pybela/](https://belaplatform.github.io/pybela/).\n", + "\n", "As with the previous tutorials, you will need to run the `potentiometers` project in Bela. If you haven't done it yet, copy the project onto Bela:" ] }, diff --git a/tutorials/notebooks/5_Sparse-timestamping.ipynb b/tutorials/notebooks/5_Sparse-timestamping.ipynb deleted file mode 100644 index 5f4b98a..0000000 --- a/tutorials/notebooks/5_Sparse-timestamping.ipynb +++ /dev/null @@ -1 +0,0 @@ -{"cells":[{"cell_type":"markdown","metadata":{},"source":["# pybela Tutorial 4: Sparse timestamping\n","In the potentiometer example used in the previous tutorials, the values for `pot1` and `pot2` are assigned at every audio frame. Let's take a look again at the `render()` loop (the Bela code for this example can be found in (in `bela-code/potentiometers/render.cpp`).\n","\n","```cpp\n","void render(BelaContext *context, void *userData)\n","{\n","\tfor(unsigned int n = 0; n < context->audioFrames; n++) {\n","\t\tif(gAudioFramesPerAnalogFrame && !(n % gAudioFramesPerAnalogFrame)) {\n","\t\t\t\n","\t\t\tuint64_t frames = context->audioFramesElapsed/gAudioFramesPerAnalogFrame + n/gAudioFramesPerAnalogFrame;\n","\t\t\tBela_getDefaultWatcherManager()->tick(frames); // watcher timestamps\n","\t\t\t\n","\t\t\tpot1 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot1Ch);\n","\t\t\tpot2 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot2Ch);\n","\t\t\t\n","\t\t}\n","\t}\n","}\n","```\n","\n","\n","The Watched clock is also \"ticked\" at every analog frame, so that the timestamps in the data correspond to the audio frames in the Bela code. The data buffers we received from Bela in the Streamer and the Logger had this form: `{\"ref_timestamp\": 92381, \"data\":[0.34, 0.45, ...]}`. Each data point is registered in the buffer every time we assign a value to `pot1` and `pot2` in the Bela code. The `ref_timestamp` corresponds to the timestamp of the first sample in the `data` array, in this case `0.34`. Since in the Bela code, we assign `pot1` and `pot2` at every audio frame, we can infer the timestamps of each value in the data array by incrementing `ref_timestamp` by 1 for each sample. \n","\n","This is an efficient way of storing data since instead of storing the timestamp of every item in the data array, we only store the timestamp of the first item. We call this *dense* timestamping. However, for many applications, we might not assign a value to a variable every frame, we might do it more than once per frame, once every few frames, or we might want to do it at irregular intervals. In these cases, we need to store the timestamp of every item in the data array. We call this *sparse* timestamping.\n","\n","In this tutorial we take a look at *sparse* timestamping. First, transfer the Bela code we will use in this tutorial to Bela:\n"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["!rsync -rvL ../bela-code/timestamping root@bela.local:Bela/projects"]},{"cell_type":"markdown","metadata":{},"source":["Then you can compile and run the project using either the IDE or by running the following command in the Terminal:\n","```bash\n","ssh root@bela.local \"make -C Bela stop Bela PROJECT=potentiometers run\" \n","```\n","(Running this on a jupyter notebook will block the cell until the program is stopped on Bela.)"]},{"cell_type":"markdown","metadata":{},"source":["As in the previous tutorials, we will use two potentiometers connected to Bela analog inputs 0 and 1. Check the `1_Streamer.ipnyb` tutorial notebook for instructions on how to set up the circuit. \n","\n","### Bela C++ code\n","\n","\n","First, let's take a look at the Bela code. First, we have added `WatcherManager::kTimestampSample` to the declaration of `pot2`. This informs the Bela Watcher that `pot2` will be watched sparsely, that is, that the watcher will store a timestamp for every value assigned to `pot2`:\n","\n","```cpp\n","Watcher pot1(\"pot1\");\n","Watcher pot2(\"pot2\", WatcherManager::kTimestampSample);\n","```\n","\n","Now let's take a look at `render()`:\n","\n","```cpp\n","void render(BelaContext *context, void *userData)\n","{\n","\tfor(unsigned int n = 0; n < context->audioFrames; n++) {\n","\t\tif(gAudioFramesPerAnalogFrame && !(n % gAudioFramesPerAnalogFrame)) {\n","\t\t\t\n","\t\t\tuint64_t frames = context->audioFramesElapsed/gAudioFramesPerAnalogFrame + n/gAudioFramesPerAnalogFrame;\n","\t\t\tBela_getDefaultWatcherManager()->tick(frames); // watcher timestamps\n","\t\t\t\n","\t\t\tpot1 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot1Ch);\n","\n","\t\t\tif (frames % 12==0){\n","\t\t\t\tpot2 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot2Ch);\n","\t\t\t}\n","\t\t}\n","\t}\n","}\n","```\n","\n","We are \"ticking\" the Bela Watcher once per analog frame, so that the timestamps in the data correspond to the analog frames in the Bela code. We are assigning a value to `pot1` at every analog frame, as in the previous examples, but we are now only assigning a value to `pot2` every 12 frames. \n","\n","### Dealing with sparse timestamps in Python\n","\n","Let's now take a look at the data we receive from Bela. We will use the Streamer. Run the cells below to declare and connect the Streamer to Bela:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["import asyncio\n","import pandas as pd\n","from pybela import Streamer"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["streamer = Streamer()\n","streamer.connect()"]},{"cell_type":"markdown","metadata":{},"source":["We can call `.list()` to take a look at the variables available to be streamed, their types and timestamp mode:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["streamer.list()"]},{"cell_type":"markdown","metadata":{},"source":["`timestampMode` indicates if the timestamping is *sparse* (1) or *dense* (0). Now let's stream the data from Bela. We will stream `pot1` and `pot2`:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["streamer.start_streaming(variables=[\"pot1\", \"pot2\"], saving_enabled=False)\n","await asyncio.sleep(2)\n","streamer.stop_streaming()"]},{"cell_type":"markdown","metadata":{},"source":["Now let's take a look at the streamed buffers for \"pot2\". Each buffer has the form `{\"ref_timestamp\": 912831, \"data\":[0.23, 0.24, ...], \"rel_timestamps\":[ 0, 12, ...]}`. `ref_timestamp` corresponds, as in the dense case, to the timestamp of the first data point in the `data` array. `rel_timestamps` is an array of timestamps relative to `ref_timestamp`. In this case, since we are assigning a value to `pot2` every 12 frames, the timestamps in `rel_timestamps` are `[0, 12, 24, 36, etc.]`."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["streamer.streaming_buffers_queue[\"pot2\"]"]},{"cell_type":"markdown","metadata":{},"source":["You can now calculate the absolute timestamps of each data point by adding the values in `rel_timestamps` to `ref_timestamp`:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["[streamer.streaming_buffers_queue[\"pot2\"][0][\"ref_timestamp\"]]*len(streamer.streaming_buffers_queue[\"pot2\"][0][\"rel_timestamps\"]) + streamer.streaming_buffers_queue[\"pot2\"][0][\"rel_timestamps\"]"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["pot2_data = {\"timestamps\":[], \"data\":[]}\n","\n","for _buffer in streamer.streaming_buffers_queue[\"pot2\"]:\n"," pot2_data[\"timestamps\"].extend([_buffer[\"ref_timestamp\"] + i for i in _buffer[\"rel_timestamps\"]])\n"," pot2_data[\"data\"].extend(_buffer[\"data\"])"]},{"cell_type":"markdown","metadata":{},"source":["Note that the timestamps are spaced by 12, as expected:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["df = pd.DataFrame(pot2_data)\n","df.head()"]},{"cell_type":"markdown","metadata":{},"source":[]}],"metadata":{"kernelspec":{"display_name":"pybela-irbKdG5b","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.9.16"},"orig_nbformat":4},"nbformat":4,"nbformat_minor":2} diff --git a/tutorials/notebooks/6_Controller.ipynb b/tutorials/notebooks/6_Controller.ipynb index d4e9bfc..8ae8270 100644 --- a/tutorials/notebooks/6_Controller.ipynb +++ b/tutorials/notebooks/6_Controller.ipynb @@ -4,11 +4,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# pybela Tutorial 5: Controller\n", + "# pybela Tutorial 6: Controller\n", "This notebook is a tutorial for the Controller class in the pybela python library. The Controller class allows you to control the variables in the Bela program using python. \n", "\n", "The Controller class has some limitations: you can only send one value at a time (no buffers) and you can not control the exact frame at which the values will be updated in the Bela program. Moreover, you can't use it at the same time as the Monitor. However, it is still a useful tool if you want to modify variable values in the Bela program without caring too much about the rate and exact timing of the updates.\n", "\n", + "The complete documentation for the pybela library can be found in [https://belaplatform.github.io/pybela/](https://belaplatform.github.io/pybela/).\n", + "\n", "As with the previous tutorials, you will need to run the `potentiometers` project in Bela. If you haven't done it yet, copy the project onto Bela:" ] }, diff --git a/tutorials/notebooks/7_Sparse-timestamping.ipynb b/tutorials/notebooks/7_Sparse-timestamping.ipynb new file mode 100644 index 0000000..41c0272 --- /dev/null +++ b/tutorials/notebooks/7_Sparse-timestamping.ipynb @@ -0,0 +1 @@ +{"cells":[{"cell_type":"markdown","metadata":{},"source":["# pybela Tutorial 7: Sparse timestamping\n","In the potentiometer example used in the previous tutorials, the values for `pot1` and `pot2` are assigned at every audio frame. Let's take a look again at the `render()` loop (the Bela code for this example can be found in (in `bela-code/potentiometers/render.cpp`).\n","\n","```cpp\n","void render(BelaContext *context, void *userData)\n","{\n","\tfor(unsigned int n = 0; n < context->audioFrames; n++) {\n","\t\tif(gAudioFramesPerAnalogFrame && !(n % gAudioFramesPerAnalogFrame)) {\n","\t\t\t\n","\t\t\tuint64_t frames = context->audioFramesElapsed/gAudioFramesPerAnalogFrame + n/gAudioFramesPerAnalogFrame;\n","\t\t\tBela_getDefaultWatcherManager()->tick(frames); // watcher timestamps\n","\t\t\t\n","\t\t\tpot1 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot1Ch);\n","\t\t\tpot2 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot2Ch);\n","\t\t\t\n","\t\t}\n","\t}\n","}\n","```\n","\n","\n","The Watched clock is also \"ticked\" at every analog frame, so that the timestamps in the data correspond to the audio frames in the Bela code. The data buffers we received from Bela in the Streamer and the Logger had this form: `{\"ref_timestamp\": 92381, \"data\":[0.34, 0.45, ...]}`. Each data point is registered in the buffer every time we assign a value to `pot1` and `pot2` in the Bela code. The `ref_timestamp` corresponds to the timestamp of the first sample in the `data` array, in this case `0.34`. Since in the Bela code, we assign `pot1` and `pot2` at every audio frame, we can infer the timestamps of each value in the data array by incrementing `ref_timestamp` by 1 for each sample. \n","\n","This is an efficient way of storing data since instead of storing the timestamp of every item in the data array, we only store the timestamp of the first item. We call this *dense* timestamping. However, for many applications, we might not assign a value to a variable every frame, we might do it more than once per frame, once every few frames, or we might want to do it at irregular intervals. In these cases, we need to store the timestamp of every item in the data array. We call this *sparse* timestamping.\n","\n","In this tutorial we take a look at *sparse* timestamping. The complete documentation for the pybela library can be found in [https://belaplatform.github.io/pybela/](https://belaplatform.github.io/pybela/).\n","\n","First, transfer the Bela code we will use in this tutorial to Bela:\n"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["!rsync -rvL ../bela-code/timestamping root@bela.local:Bela/projects"]},{"cell_type":"markdown","metadata":{},"source":["Then you can compile and run the project using either the IDE or by running the following command in the Terminal:\n","```bash\n","ssh root@bela.local \"make -C Bela stop Bela PROJECT=potentiometers run\" \n","```\n","(Running this on a jupyter notebook will block the cell until the program is stopped on Bela.)"]},{"cell_type":"markdown","metadata":{},"source":["As in the previous tutorials, we will use two potentiometers connected to Bela analog inputs 0 and 1. Check the `1_Streamer.ipnyb` tutorial notebook for instructions on how to set up the circuit. \n","\n","### Bela C++ code\n","\n","\n","First, let's take a look at the Bela code. First, we have added `WatcherManager::kTimestampSample` to the declaration of `pot2`. This informs the Bela Watcher that `pot2` will be watched sparsely, that is, that the watcher will store a timestamp for every value assigned to `pot2`:\n","\n","```cpp\n","Watcher pot1(\"pot1\");\n","Watcher pot2(\"pot2\", WatcherManager::kTimestampSample);\n","```\n","\n","Now let's take a look at `render()`:\n","\n","```cpp\n","void render(BelaContext *context, void *userData)\n","{\n","\tfor(unsigned int n = 0; n < context->audioFrames; n++) {\n","\t\tif(gAudioFramesPerAnalogFrame && !(n % gAudioFramesPerAnalogFrame)) {\n","\t\t\t\n","\t\t\tuint64_t frames = context->audioFramesElapsed/gAudioFramesPerAnalogFrame + n/gAudioFramesPerAnalogFrame;\n","\t\t\tBela_getDefaultWatcherManager()->tick(frames); // watcher timestamps\n","\t\t\t\n","\t\t\tpot1 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot1Ch);\n","\n","\t\t\tif (frames % 12==0){\n","\t\t\t\tpot2 = analogRead(context, n/gAudioFramesPerAnalogFrame, gPot2Ch);\n","\t\t\t}\n","\t\t}\n","\t}\n","}\n","```\n","\n","We are \"ticking\" the Bela Watcher once per analog frame, so that the timestamps in the data correspond to the analog frames in the Bela code. We are assigning a value to `pot1` at every analog frame, as in the previous examples, but we are now only assigning a value to `pot2` every 12 frames. \n","\n","### Dealing with sparse timestamps in Python\n","\n","Let's now take a look at the data we receive from Bela. We will use the Streamer. Run the cells below to declare and connect the Streamer to Bela:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["import asyncio\n","import pandas as pd\n","from pybela import Streamer"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["streamer = Streamer()\n","streamer.connect()"]},{"cell_type":"markdown","metadata":{},"source":["We can call `.list()` to take a look at the variables available to be streamed, their types and timestamp mode:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["streamer.list()"]},{"cell_type":"markdown","metadata":{},"source":["`timestampMode` indicates if the timestamping is *sparse* (1) or *dense* (0). Now let's stream the data from Bela. We will stream `pot1` and `pot2`:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["streamer.start_streaming(variables=[\"pot1\", \"pot2\"], saving_enabled=False)\n","await asyncio.sleep(2)\n","streamer.stop_streaming()"]},{"cell_type":"markdown","metadata":{},"source":["Now let's take a look at the streamed buffers for \"pot2\". Each buffer has the form `{\"ref_timestamp\": 912831, \"data\":[0.23, 0.24, ...], \"rel_timestamps\":[ 0, 12, ...]}`. `ref_timestamp` corresponds, as in the dense case, to the timestamp of the first data point in the `data` array. `rel_timestamps` is an array of timestamps relative to `ref_timestamp`. In this case, since we are assigning a value to `pot2` every 12 frames, the timestamps in `rel_timestamps` are `[0, 12, 24, 36, etc.]`."]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["streamer.streaming_buffers_queue[\"pot2\"]"]},{"cell_type":"markdown","metadata":{},"source":["You can now calculate the absolute timestamps of each data point by adding the values in `rel_timestamps` to `ref_timestamp`:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["[streamer.streaming_buffers_queue[\"pot2\"][0][\"ref_timestamp\"]]*len(streamer.streaming_buffers_queue[\"pot2\"][0][\"rel_timestamps\"]) + streamer.streaming_buffers_queue[\"pot2\"][0][\"rel_timestamps\"]"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["pot2_data = {\"timestamps\":[], \"data\":[]}\n","\n","for _buffer in streamer.streaming_buffers_queue[\"pot2\"]:\n"," pot2_data[\"timestamps\"].extend([_buffer[\"ref_timestamp\"] + i for i in _buffer[\"rel_timestamps\"]])\n"," pot2_data[\"data\"].extend(_buffer[\"data\"])"]},{"cell_type":"markdown","metadata":{},"source":["Note that the timestamps are spaced by 12, as expected:"]},{"cell_type":"code","execution_count":null,"metadata":{},"outputs":[],"source":["df = pd.DataFrame(pot2_data)\n","df.head()"]},{"cell_type":"markdown","metadata":{},"source":[]}],"metadata":{"kernelspec":{"display_name":"pybela-irbKdG5b","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.9.16"},"orig_nbformat":4},"nbformat":4,"nbformat_minor":2}