In my last post, I introduced the Ultraviolet virtual machine, a Smalltalk-like virtual machine for creating Arduino Audio applications. We previously created a small programming language called Bell that compiled to the Ultraviolent virtual machine. However, since then Steve wrote a post about adding knobs to our guitar pedal. Well, now we need to catch up and extend Bell to allow for knob input too.
The way that Steve implemented the knobs was with potentiometers and using the onboard analog-to-digital converter (ADC) on the microcontroller to do an analog read of the potentionmeter. There are a lot of ways that you could integrate this sort of functionality into an Arduino program and the way that Steve's code did it is standard and totally fine. However, there are some subtleties that I would like to examine when implementing our take on this problem in the Bell language.
For Bell, I introduced a new microformat into the language for this use case: properties. A property in this context is an aspect of an object that is updated in a functional way, avoiding the need to deal with state explicitly. I hope this will make more sense once you see the implementation, but the overall goal is to avoid doing things the way we do them in arduino, which would be to call the analogRead() function in order to trigger a reading from the ADC. There's nothing wrong with doing it that way, but it's a very imperative, stateful way of doing things. With the "property" extension to Bell, I've wrapped up everything imperative, stateful, and mutating and separated it out from your application so that you can write pure functional code.
Here is an example of using "property" in a Bell program:
instance codec : AudioControlSGTL5000
instance tonesweep : AudioSynthToneSweep
instance i2s : AudioOutputI2S
flow tonesweep -> i2s
object main uses codec tonesweep
event main setup uses codec tonesweep : codec enable . codec volume 0.5 0.5 . tonesweep play 0.8 256 512 10
property main speed : AnalogRead A0
property main volume : AnalogRead A1
property main depth : AnalogRead A2
property main shape : AnalogRead A3
As you can see, properties go on objects, each property has a name, and it also has a initializer. In this case, the initializer is a new type of effect module, AnalogRead. I also introduce here the idea of effect module parameters. The previous effect modules didn't take parameters. AudioOutputI2S and AudioOutputAnalog, for instance, are two completely separate modules. However, for AnalogRead, you have a lot of pins that you can use for doing an analogRead, so the number of different effects is large. Some Arduinos have over 50. I thought it might look cleaner to have one AnalogRead effect that takes a pin parameter, such as "AnalogRead A0". However, this is really just syntax. Each pin is essentially its own independent effect.
The first code generation that we do with properties is to add C++ instance properties to our C++ class for each Bell property on the corresponding Bell object. Here is the main.hpp code that we would generate for this program:
#ifndef _MAIN_H_
#define _MAIN_H_
#include <Arduino.h>
#include "Audio.h"
#include "AudioControlSGTL5000Universe.h"
#include "AudioSynthToneSweepUniverse.h"
class Main
{
public:
Main(AudioControlSGTL5000Universe *codec, AudioSynthToneSweepUniverse *tonesweep) : codec(codec), tonesweep(tonesweep) {}
void setup();
int speed = 0;
int volume = 0;
int depth = 0;
int shape = 0;
private:
AudioControlSGTL5000Universe *codec;
AudioSynthToneSweepUniverse *tonesweep;
};
#endif
As you can see, there is an instance property on the class for each Bell property.
Next, let's look at the .ino file that is generated:
#include <Arduino.h>
#include "Audio.h"
#include "main.hpp"
#include "AudioControlSGTL5000Module.h"
#include "AudioControlSGTL5000Universe.h"
#include "AudioSynthToneSweepModule.h"
#include "AudioSynthToneSweepUniverse.h"
#include "AudioOutputI2SModule.h"
#include "AudioOutputI2SUniverse.h"
AudioControlSGTL5000 codec;
AudioControlSGTL5000Module codecModule(&codec);
AudioControlSGTL5000Universe codecUniverse(&codecModule);
AudioSynthToneSweep tonesweep;
AudioSynthToneSweepModule tonesweepModule(&tonesweep);
AudioSynthToneSweepUniverse tonesweepUniverse(&tonesweepModule);
AudioOutputI2S i2s;
AudioOutputI2SModule i2sModule(&i2s);
AudioOutputI2SUniverse i2sUniverse(&i2sModule);
Main main(&codecUniverse, &tonesweepUniverse);
AudioConnection connection0_0a(tonesweep, 0, i2s, 0);
AudioConnection connection0_0b(tonesweep, 1, i2s, 1);
void setup()
{
main.setup();
pinMode(A1, INPUT);
pinMode(A3, INPUT);
pinMode(A0, INPUT);
pinMode(A2, INPUT);
}
void loop()
{
analogReadHandler();
}
void analogReadHandler()
{
int analogReadResultA1 = analogRead(A1);
int analogReadResultA3 = analogRead(A3);
int analogReadResultA0 = analogRead(A0);
int analogReadResultA2 = analogRead(A2);
AudioNoInterrupts();
main->speed = analogReadResultA0;
main->volume = analogReadResultA1;
main->depth = analogReadResultA2;
main->shape = analogReadResultA3;
AudioInterrupts();
}
Here we get into some of the subtleties of doing analogRead() calls on Arduino. The first thing we do in code generation is create a set of all of the pins that we need to read. These pins can be shared among multiple objects that depend on their values, but each object is going to get its own read-only copy of the value. We only do we read per pin, no matter how many consumers of the read value depend on it. The next thing we do is call pinMode() for each pin we'll be using in setup(). This ensures that the pin is in INPUT mode and not OUTPUT mode. Then in the loop() function we call our generated analogReadHandler(), which will do all of the real business of analogRead() handling. First we do all the reads for each pin, and we do each read only once through the loop(). In the next section of code, we provide copies of the read values to all of the objects that depend on these values. This is carefully done sandwiched between AudioNoInterrupts() / AudioInterrups() calls so that the audio engine is paused while we update the C++ instance properties with the new read values. This ensures that the code behaves in a functional way with safe concurrency. The audio engine's update() functions for each audio effect in the chain will not be called while we are updating instance properties. This means that, from the perspective of the audio engine, these writes are atomic. This avoids weird glitches that could occur if you were able to update the value in the middle of an audio effect update of an audio block.
A nice feature of Bell is that it writes your C++ code correctly for you, so you don't need to know all of the pitfalls of writing correct C++ code to interface with the audio engine without audible glitches and other subtle issues. This is the main value that I hope it will provide to future users of our pedal hardware: correct code with less burden of background knowledge on the programmer.
Currently, our Bell / Ultraviolet code is lagging behind Steve as he continues to improve his implementation of a tremolo pedal. With each new demand on the capabilities of the language and virtual machine, I strive to find the right balance of adding new functionlity without making the overall end product too complex. There is a lot of research and design that goes into each new feature, with several options to consider and ultimately reject all but one. Things are going pretty well, with the total codebase size still less than 3000 lines. However, we've still got a while to go in order to implement a tremolo effect. For instance, our new property syntax allows us to have analog knob values available inside of an object, but we have yet to provide a mechanism in Bell to read those values. Additionally, Steve does a transform on some of the values, dividing by a constant in order to reduce the overall range. Adding numerical operations like this will be the focus of the next post. It would also be nice to add some debouncing or otherwise smoothing code to the AnalogRead implementation. So lots to do still, certainly!