-
Notifications
You must be signed in to change notification settings - Fork 139
Example projects and tutorials
You can access the example projects through the Example Projects tab of the Bela IDE. Each example also has an accompanying example document that leads you through the example step-by-step. You can get to this document by clicking on the example doc button in the top righthand corner of the text editor.
Below is a list of the sample sketches, and what they do.
A collection of Bela files are called a "project".
If you open a project folder in the above repository, you'll see that each of these Bela project contains at least one file, render.cpp (some projects have additional files, but every project has at least this). The main.cpp is the second most common file to see in a project but you don't really have to worry about; it contains helper functions and things that run command line arguments. Most work is done in the render.cpp file.
A render.cpp file has three functions: setup()
, render()
and cleanup()
.
setup()
is an initialisation function which runs before audio rendering begins.
It is called once when the project starts. Use it to prepare any memory or
resources that will be needed in render()
.
render()
is a function that is regularly called, over and over continuously, at
the highest priority by the audio engine. It is used to process audio and
sensor data. This function is called regularly by the system every time there
is a new block of audio and/or sensor data to process.
cleanup()
is a function that is called when the program stops, to finish up any
processes that might still be running.
Here we will briefly explain each function and the structure of the render.cpp
At the top of the file, include any libraries you might need.
Additionally, declare any global variables. In these tutorial sketches, all
global variables are preceded by a g
so we always know which variables are
global - gSampleData
, for example. It's not mandatory but is a really good way
of keeping track of what's global and what's not.
Sometimes it's necessary to access a variable from another file, such as
main.cpp. In this case, precede this variable with the keyword extern
.
setup()
, render()
and cleanup()
each take the same arguments. These are:
BelaContext *context
void *userData
These arguments are pointers to data structures. The main one that's used is
context
, which is a pointer to a data structure containing lots of information
you need.
Take a look at what's in the data structure in the API reference tab.
You can access any of these bits of information about current audio and sensor
settings and pointers to data buffers that are contained in the data structure
like this: context->name_of_item
.
For example, context->audioInChannels
returns the number of audio input channels.
context->audioSampleRate
returns the audio sample rate.
context->audioIn[n]
would give you the current input sample (assuming that
your input is mono - if it's not you will have to account for multiple channels).
Note that audioIn
, audioOut
, analogIn
, analogOut
are all arrays (buffers).
This sketch demonstrates how to read from and write to the audio and analog input and output buffers.
In render()
you'll see a nested for loop structure. You'll see this in all Bela projects.
The first for loop cycles through audioFrames
, the second through
audioInChannels
(in this case left 0 and right 1).
You can access any information about current audio and sensor settings like this:
context->name_of_item
. For example context->audioInChannels
returns current number of input channels,
context->audioFrames
returns the current number of audio frames,
context->audioSampleRate
returns the audio sample rate.
You can look at all the information you can access in ::BelaContext.
The simplest way to read samples from the audio input buffer is with
audioRead()
which we pass three arguments: context, current audio
frame and current channel. In this example we have
audioRead(context, n, ch)
where both n
and ch
are provided by
the nested for loop structure.
We can write samples to the audio output buffer in a similar way using
audioWrite()
. This has a fourth argument which is the value of to output.
For example audioWrite(context, n, ch, value_to_output)
.
The same is true for analogRead()
and analogWrite()
.
Note that for the analog channels we write to and read from the buffers in a separate set
of nested for loops. This is because the they are sampled at half audio rate by default.
The first of these for loops cycles through analogFrames
, the second through
analogInChannels
.
By setting audioWriteFrame(context, n, ch, audioReadFrame(context, n, ch))
and
analogWrite(context, n, ch, analogReadFrame(context, n, ch))
we have a simple
passthrough of audio input to output and analog input to output.
It is also possible to address the buffers directly, for example:
context->audioOut[n * context->audioOutChannels + ch]
.
This sketch is the hello world of embedded interactive audio. Better known as bleep, it produces a sine tone.
The frequency of the sine tone is determined by a global variable, gFrequency
.
The sine tone is produced by incrementing the phase of a sin function
on every audio frame.
In render() you'll see a nested for loop structure. You'll see this in all Bela projects. The first for loop cycles through 'audioFrames', the second through 'audioChannels' (in this case left 0 and right 1). It is good to familiarise yourself with this structure as it's fundamental to producing sound with the system.
This example demonstrates the scope feature of the IDE.
The scope is instantiated at the top of the file via Scope scope;
In setup()
we define how many channels the scope should have and the sample
rate that it should run at via scope.setup(3, context->audioSampleRate)
.
In render()
we choose what the scope log via scope.log(out, out2, out3)
.
In this example the scope is logging three sine waves with different phases. To see
the output click on the Open Scope button.
An additional option is to set the trigger of the oscilloscope from within render()
.
In this example we are triggering the scope when oscillator 1 becomes less than
oscillator 2 via scope.trigger()
. Note that this functionality only takes effect
when the triggering mode is set to custom in the scope UI.
This example demonstrates how to print to the console. When working within the audio thread use the function rt_printf(). This has the same functionality as printf() but is safe to call from the audio thread. However, make sure to not make too many calls to this function within a render loop as this may overload the CPU and/or stall communication with the board. In the render() function above a counter is implemented in order to only print to the console after a specified interval has passed. A counter variable is used to keep track of the amount of samples elapsed after starting the program. The usage of rt_printf() is identical to printf(): http://en.cppreference.com/w/cpp/io/c/fprintf
This sketch shows the simplest case of digital out.
- Connect an LED in series with a 470ohm resistor between P8_07 and ground.
The led is blinked on and off by setting the digital pin HIGH
and LOW
every interval seconds which is set in
render()
.
In setup()
the pin mode must be set to output mode via pinMode()
. For example:
pinMode(context, 0, P8_07, OUTPUT)
.
In render()
the output of the digital pins is set by digitalWrite()
. For example:
digitalWrite(context, n, P8_07, status)
where status
can be equal to
either HIGH
or LOW
. When set HIGH
the pin will give 3.3V, when set to
LOW
0V.
Note that there are two ways of specifying the digital pin: using the GPIO label (e.g. P8_07
), or using the digital IO index (e.g. 0)
To keep track of elapsed time we have a sample counter count. When the count reaches
a certain limit it switches state to either HIGH
or LOW
depending on its current
value. In this case the limit is context->digitalSampleRate*interval
which
allows us to write the desired interval in seconds, stored in interval
.
This example brings together digital input and digital output. The program will read a button and turn the LED on and off according to the state of the button.
- connect an LED in series with a 470ohm resistor between P8_07 and ground.
- connect a 1k resistor to P9_03 (+3.3V),
- connect the other end of the resistor to both a button and P8_08
- connect the other end of the button to ground.
Before using the digital pins we need to set whether they are input or output.
This is done via pinMode(context, 0, P8_08, INPUT);
.
You will notice that the LED will normally stay off and will turn on as long as
the button is pressed. This is due to the fact that the LED is set to the same
value read at input P8_08. When the button is not pressed, P8_08 is LOW
and so
P8_07 is set to LOW
as well, so that the LED conducts and emits light. When
the button is pressed, P8_08 goes HIGH
and P8_07 is set to HIGH
, turning off the LED.
Note that there are two ways of specifying the digital pin: using the GPIO label (e.g. P8_07
),
or using the digital IO index (e.g. 0)
As an exercise try and change the code so that the LED only turns off when the button is pressed.
This sketch shows the simplest case of digital out.
- Connect an LED in series with a 470ohm resistor between every Digital pin 0 - 9 and ground.
This sketch produces a sine tone, the frequency and amplitude of which are
modulated by data received on the analog input pins. Before looping through each audio
frame, we declare a value for the frequency
and amplitude
of our sine tone;
we adjust these values by taking in data from analog sensors (for example potentiometers)
with analogRead()
.
- connect a 10K pot to 3.3V and GND on its 1st and 3rd pins.
- connect the 2nd middle pin of the pot to analogIn 0.
- connect another 10K pot in the same way but with the middle pin connected to analogIn 1.
The important thing to notice is that audio is sampled twice as often as analog data. The audio sampling rate is 44.1kHz (44100 frames per second) and the analog sampling rate is 22.05kHz (22050 frames per second). Notice that we are processing the analog data and updating frequency and amplitude only on every second audio sample, since the analog sampling rate is half that of the audio.
if(!(n % gAudioFramesPerAnalogFrame)) {
// Even audio samples: update frequency and amplitude from the matrix
frequency = map(analogRead(context, n/gAudioFramesPerAnalogFrame, gSensorInputFrequency), 0, 1, 100, 1000);
amplitude = analogRead(context, n/gAudioFramesPerAnalogFrame, gSensorInputAmplitude);
}
This sketch uses a sine wave to drive the brightness of a series of LEDs
connected to the eight analog out pins. Again you can see the nested for
loop
structure but this time for the analog output channels rather than the audio.
- connect an LED in series with a 470ohm resistor between each of the analogOut pins and ground.
Within the first for loop in render we cycle through each frame in the analog output matrix. At each frame we then cycle through the analog output channels with another for loop and set the output voltage according to the phase of a sine tone that acts as an LFO. The analog output pins can provide a voltage of ~4.092V.
The output on each pin is set with analogWrite()
within the for loop that
cycles through the analog output channels. This needs to be provided with
arguments as follows analogWrite(context, n, channel, out)
. Channel is
where the you give the address of the analog output pin (in this case we cycle
through each pin address in the for loop), out is the variable that holds the
desired output (in this case set by the sine wave) and n
is the frame number
(given by the other for loop).
Notice that the phase of the brightness cycle for each led is different. This is achieved by updating a variable that stores a relative phase value. This variable is advanced by pi/4 (1/8 of a full rotation) for each channel giving each of the eight LEDs a different phase.
This example reads from analogue inputs 0 and 1 via analogRead()
and
generates a sine wave with amplitude and frequency determined by their values.
It's best to connect a 10K potentiometer to each of these analog inputs. Far
left and far right pins of the pot go to 3.3V and GND, the middle should be
connected to the analog in pins.
The sine wave is then plotted on the oscilloscope. Click the Open Scope button to view the results. As you turn the potentiometers you will see the amplitude and frequency of the sine wave change. You can also see the two sensor readings plotted on the oscilloscope.
The scope is initialised in setup()
where the number of channels and sampling rate
are set.
scope.setup(3, context->audioSampleRate);
We can then pass signals to the scope in render()
using:
scope.log(out, gIn1, gIn2);
This project also shows as example of map()
which allows you to re-scale a number
from one range to another. Note that map()
does not constrain your variable
within the upper and lower limits. If you want to do this use the constrain()
function.
This example demonstrates how to hook up a Light Dependent Resistor
(LDR), also know as a photo-resistor, and use it to control the
volume of white noise. If you run this project straight away
after connecting up the LDR you will notice that the LDR reacts
to changes in the light condition but in a pretty unsatisfying way.
This is because we need to set the thresholds of the map()
function
based on the ambient light condition.
To begin let's connect the LDR.
- connect one leg of the LDR to 3.3V
- connect the other leg of the LDR to analog input 1
- connect one leg of a 10KOhm resistor to this leg of the LDR as well
- connect the other leg of the resistor to ground
The resistance of the LDR changes depending on the amount of light it receives (more light -> less resistance). To measure this change in resistance we need a fixed value resistor (10kOhms in this example) to compare the reading with. This is know as a voltage divider circuit. The 3.3V will be shared between the two resistors: how much of a share of voltage each of the resistors take is proportional to their resistances. As the resistance of the LDR changes the amount of voltage on each resistor changes and we can measure this change to tell how much light the LDR is receiving.
In order to use the LDR as a volume control we need to set the thresholds
for ambient light and for when it has a bright light shone close to it.
To do this comment out this section of code in render()
:
if(!n%2){
if(gSampleCount >= 44100) {
rt_printf("%f\n", analogRead(context, n, 1));
gSampleCount = 0;
}
}
gSampleCount++;
This prints the value of the LDR reading to the console once a second.
Now you can set the variables gDark
and gLight
with the reading
of ambient light in the room and with the reading when a torch is
shone directly at the LDR. Update the variables, re-comment out the code and
run the example. Now you should be able to bring the white noise from
silence to full volume depending on the amount light.
Note that when you cover the LDR you should also hear an increase in
volume of the white noise. This is because the map()
function is not
constrained which means that it outputs negative number when it gets
darker than the threshold set in gDark
. To stop this behaviour you
can use the constrain()
function to force the LDR readings to remain
within a certain range (for example gDark
to gLight
).
This sketch allows you to hook up an MPR121 capactive touch sensing device to Bela, for example the SparkFun Capacitive Touch Sensor Breakout - MPR121. The breakout board gives you 12 electrode connections.
To get this working with Bela you need to connect the breakout board to the I2C terminal on the Bela board. See the Pin guide for details of which pin is which.
The sensor data will then be available for you to use in the array
sensorValue[NUM_TOUCH_PINS]
.
This sketch shows how to playback audio samples from a buffer using onset detection of strikes detected by a piezo sensor.
An audio file is loaded into a buffer SampleData
as gSampleData
. This is
accessed with a read pointer that is incremented at audio rate within the render
function: out += gSampleData.samples[gReadPtr++]
.
Note that the read pointer is stopped from incrementing past the length of the
gSampleData
. This is achieved by comparing the read pointer value against the
sample length which we can access as follows: gSampleData.sampleLen
.
The piezo is connected to Bela through a simple voltage divider circuit.
- Connect a 1.8 MOhm resistor between the positive (red) and negative (black) leads of the piezo
- Also connect the negative side to ground
- Connect analog input 0 to the positive side
- Connect another 1.8 MOhm resistor between positive and 3.3V
In order to get a coherent trigger from the piezo disk we have to go through a few stages of signal taming. The first is a DC offset filter which recentres the signal around 0. This is necessary as our voltage divider circuit pushes the piezo input signal to half the input voltage range, allowing us to read the piezo's full output.
As a piezo disk behaves like a microphone it outputs both negative and positive values. A second step we have to take before detecting strikes is to fullwave rectify the signal, this gives us only positive values.
Next we perform the onset detection. We do this by looking for a downwards trend in the sensor data
after a rise. Once we've identified this we can say that a peak has occured and trigger the sample
to play. We do this by setting gReadPtr = 0;
.
This type of onset detection is by no means perfect. Really we should lowpass filter the piezo signal before performing the onset detection algorithm and implement some kind of debounce on the stikes to avoid multiple strikes being detected for a single strike.