Skip to content

Commit

Permalink
v1.0.0! added tutorial bela2python2bela, minor fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
pelinski committed Aug 9, 2024
1 parent 3e2aaa7 commit 02722ac
Show file tree
Hide file tree
Showing 21 changed files with 649 additions and 160 deletions.
6 changes: 3 additions & 3 deletions docs/docs-readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
23 changes: 8 additions & 15 deletions pybela/Streamer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions pybela/Watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
10 changes: 3 additions & 7 deletions readme.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -85,7 +85,7 @@ scp watcher/Watcher.h watcher/Watcher.cpp [email protected]: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.
Expand Down Expand Up @@ -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?
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setuptools.setup(
name="pybela",
version="0.1.0",
version="1.0.0",
author="Teresa Pelinski",
author_email="[email protected]",
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.",
Expand Down
18 changes: 4 additions & 14 deletions test/bela-test-send/render.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@ ReceivedBuffer receivedBuffer;
uint receivedBufferHeaderSize;
uint64_t totalReceivedCount;

struct CallbackBuffer {
uint32_t guiBufferId;
std::vector<float> 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) {

Expand All @@ -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];
}
}

Expand All @@ -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);
Expand Down
14 changes: 13 additions & 1 deletion test/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 [email protected]:Bela/projects/
rsync -rvL test/bela-test test/bela-test-send [email protected]:Bela/projects/
ssh [email protected] "make -C Bela stop Bela PROJECT=bela-test run"
```

Expand All @@ -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 [email protected] "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
```

1 change: 1 addition & 0 deletions tutorials/bela-code/bela2python2bela/Watcher.cpp
1 change: 1 addition & 0 deletions tutorials/bela-code/bela2python2bela/Watcher.h
137 changes: 137 additions & 0 deletions tutorials/bela-code/bela2python2bela/render.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#include <Bela.h>
#include <Watcher.h>
#include <cmath>
#include <vector>
#include <RtThread.h>

#define NUM_OUTPUTS 2
#define MAX_EXPECTED_BUFFER_SIZE 1024

Watcher<float> pot1("pot1");
Watcher<float> pot2("pot2");

uint gPot1Ch = 0;
uint gPot2Ch = 1;

std::vector<std::vector<float>> 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<float> 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) {
}
1 change: 0 additions & 1 deletion tutorials/bela-code/potentiometers/sketch.js

This file was deleted.

1 change: 0 additions & 1 deletion tutorials/bela-code/timestamping/sketch.js

This file was deleted.

Loading

0 comments on commit 02722ac

Please sign in to comment.