Roger B. Dannenberg

Home Publications Videos Opera Audacity
Dannenberg playing trumpet.
Photo by Alisa.

O2host and Browser-Microcontroller Communication


Summary

O2host provides a simple way to connect browsers and microcontrollers to OSC, MIDI, and other browsers and microcontrollers. This installment shows how the o2host program can be used to communicate between a browser and a micocontroller.

About the Author

Roger B. Dannenberg is Emeritus Professor of Computer Science at Carnegie Mellon University and a Fellow of the Association for Computing Machinery. He is known for a broad range of research in Computer Music, including the creation of interactive computer accompaniment systems, languages for computer music, music understanding systems, and music composing software. He is a co-creator of Audacity, perhaps the most widely used music editing software.

Additional References

O2 source code is open and free.

A video prepared for ICMC 2022 demonstrates location independence and discovery features of O2.

Building Music Systems with O2 and O2lite is an introduction to O2.

Using O2host with O2lite and MicroPython is the first article in this series on o2host.

Controlling Soundcool with a Web Browser Using O2host is a related article in this series on o2host.

Articles on O2 are listed in my bibliography.

Our task is to hook a browser up to a microcontroller, preferably without running an entire web server on the microcontroller, which we want to keep simple.

Why would we do this? Adding interfaces to microcontrollers is hard. Typically, I modify program microcontroller code directly, upload the changes and test. It’s hard to tweak parameters, so usually I send raw data from the microcontroller to some nicer programming environment where I can interactively tune the behavior, such as input-to-output mappings, to my liking.

In this little project, I am going to create a little program to send MIDI data in response to accelerometer data. To “tune” the response, I will use a graphical interface in a browser to set parameters remotely in the microcontroller. To get MIDI out, I will use O2 messages. Browser-to-microcontroller and microcontroller-to-MIDI will be implemented using o2host, which already supports these tasks.

Here is what the interface looks like along with a short video demonstration of the entire system: microcontroller (ESP32 Thing from SparkFun), o2host, web page, and MIDI synthesizer:

Figure 1.

Is this simpler than writing a special program for sensor data mapping? Well, that might require three programs: the microcontroller program for sensor data, a mapping program for mapping, and a browser program for the graphical interface. Maybe this approach is a little simpler.

The MIDI Controller Program

On the microcontroller, I'll use two degrees of freedom from an accelerometer. One axis, roll, will control pitch. The other, pitch, will enable or disable note production. We will parameterize the controller with the following:

About Accelerometers

Here, the “pitch” axis refers to a rotation dimension, not musical pitch: if you lean forward or lean back, you are rotating along the pitch axis.

Our sensors take advantage of constant gravitational acceleration. Gravity will produce acceleration along 3 different axes, which are sensed independently. The side-to-side acceleration is zero when the controller is level, but when you roll (slowly) to the left, the sensor detects a gravitational acceleration to the left, so we can report that as some amount of roll. Similarly, pitching forward applies some gravity in the front-to-back direction, so we have an independent sense of pitch. The third axis, yaw, corresponds to rotating the controller to face a different direction, which does not change anything sensed by the accelerometer. Therefore, we only have 2 axes to work with.

In principle, you can also integrate a gyroscope’s output to estimate yaw, but the math is complex and errors accumulate. Also, you can shake the controller, which will introduce acceleration in addition to gravity. This might produce some interesting effects, but we will make no plans for this and just see what happens.

Implementation

The entire project is open and free to inspect, including the o2host program, which is part of the O2 system, which is shared on Github. Sources for this project are here.

I was going to use Micropython, but I needed access to the accelerometer, and I already had code to do that in C++, so I did the whole ESP32 implementation in C++. I will focus on the “interesting” parts: communicating via O2lite and turning the accelerometer data into MIDI.

O2lite Communication

O2lite is a simple message-passing protocol. A feature is automatic discovery, so configuration is simpler than worrying about ports and IP addresses. Initialization looks like this (I discovered that the Wi-Fi library will no longer connect to older routers using WEP security, unless you add the setMinSecurity() call). "rbdapp" is the O2 ensemble name used to prevent interference with any other O2 application. You might want to personalize the name:

void setup()
{
  // Initilize hardware:
  Serial.begin(115200);
  Serial.println("calling setMinSecurity");
  WiFi.setMinSecurity(WIFI_AUTH_WEP);
  connect_to_wifi("esp", networkName, networkPswd);
  o2l_initialize("rbdapp");
  ...

This gets O2lite running and looking for a host, so you will have to run o2host (available from github).

To receive control information from the browser, we need to set up a “service” and some message handlers. The service is the top-level node of an O2 address. After creating the “midiapp” service, we create some handlers. I will show just the handler for limit which is a float parameter used in mapping from accelerometer data to pitch:

  ...
  o2l_set_services("midiapp");
  // Map from -axlimit to +axlimit to the pitch range
  o2l_method_new("/midiapp/limit", "f", true, set_limit, NULL);
  ...

The parameters mean you want to handle messages to the address "/midiapp/limit", the message will contain one float ("f"), the address and types must match exactly (true), the handler is implemented by the function set_limit, and an extra parameter to pass to the handler is given by NULL. We must provide the handler function, which simply unpacks the float value from the message and stores it in limit for use later:

...
float limit = 1.0;
...

...
void set_limit(o2l_msg_ptr msg, const char *types, void *data, void *info)
{
    limit = o2l_get_float();
    Serial.println("/midiapp/limit "); Serial.println(limit);
}
...

Creating MIDI

My code to create a stream of notes is pretty simple. First, we need to decide when to play a note. This is done inside loop which is automatically called frequently. The basic idea is to just wait until beat_time and then increment beat_time by beat_dur and wait again. One detail is that if loop every gets delayed for many beat durs, we could output many notes trying to “catch up” to now. So every time we play a note, we increment beat_time by dur as many times as needed (in the while loop) to “catch up.” After along delay, this approach will not output a burst of notes to make up for lost time. Here’s the code:

void loop()
{
    ...
    o2l_poll();
    imu_poll();
    double now = o2l_local_time();
    if (beat_time < now) {
        compute_beat();
        // advance to next beat AFTER current time
        while (beat_time < now) {
            beat_time += beat_dur;
        }
    }
}

Now for the fun part: mapping accelerometer data to make notes. Again, the algorithm here is very simple: First, we test if there was a previous note, and if so, we construct and send a note-off message for it. If pitch is zero, it means there is no note to turn off.

Then, we test the forward tilt in imu_ay (I will use “forward tilt” to avoid saying “pitch” which is confusing in this context) and if it is above thresh we do not want to make a new note, setting pitch = 0;. Otherwise, the roll data (in imu_ax) is clipped to plus-or-minus limit and interpolated to the range of min_pitch to max_pitch, and we call send_note to send it. We save pitch into the global prev_pitch so when we return to compute_beat we will know whether there is a sounding note to turn off:

// this is the "compositional algorithm" that constructs
// MIDI output from sensor data
void compute_beat()
{
    int pitch;
    if (prev_pitch > 0) {
        send_note(prev_pitch, 0);  // note off
    }
    if (imu_ay > thresh) {
        pitch = 0;  // disabled by roll
    } else {
        // use imu_ax for pitch and imu_ay for on/off
        if (imu_ax < -limit) imu_ax = -limit;
        else if (imu_ax > limit) imu_ax = limit;
        pitch = (max_pitch - min_pitch) * (limit - imu_ax) / (2 * limit) +
                min_pitch;
        send_note(pitch, 100);  // note on
        Serial.print("noteon ");
        Serial.print(pitch);
        Serial.print(" ");
        Serial.println(o2l_local_time());
    }
    prev_pitch = pitch;
}

Finally, send_note maps its pitch parameter to the desired scale using 12-element arrays telling whether the pitch mod 12 is in the scale or not. Then we pack a 3-byte MIDI message into an integer (the bytes are NOTE-ON=0x90, the pitch, and the velocity), add this to a message, and send it. O2 takes care of routing the message to the "midiout" service:

void send_note(int pitch, int vel)
{
    // adjust the pitch until it is in the current scale
    int pc = pitch % 12;
    // scales are numbered 1 to 4, but index is 0 to 3:
    while (!scalemap[scale - 1][pc]) {
       pitch++;
       pc = pitch % 12;
    }
    // transpose out-of-range pitch if necessary:
    while (pitch > 127) pitch -= 12;;
    while (pitch < 0) pitch += 12;
    o2l_send_start("/midiout/midi", 0, "i", 1);  // tcp
    o2l_add_int32(NOTE_ON + (pitch << 8) + (vel << 16));
    o2l_send();
}

The Browser Interface

The browser interface is constructed in a small web page served by o2host. There is nothing special about it except that it sends O2 messages when controls are changed, so let’s look at that code. (You can find the full sources in a link above.) Since we looked at handlers for limit above, let’s see how the messages are generated, starting with the slider itself:

<table>
<tr><td><b>Roll Input Range</b></td>
    <td><input type="range" min="0" max="20" value="10" class="slider"
      id="rollrange" oninput="rollrange_input(this.value)"></td>
    <td><big id=rollrange_dec>1.0</big></td></tr>
...
</table>

This calls rollrange_input with a slider value from 0 to 20, and here is the function. Notice that we divide by 10 to scale the integer 0-20 scale to the desired 0-2 scale, and we write the numerical value into the last column of the table. Sending the value via O2 is a one-liner at the end (0 is a timestamp that means send immediately):

function rollrange_input(value) {
    var rollrange_dec = document.getElementById("rollrange_dec");
    value = value / 10.0;
    rollrange_dec.innerHTML = "" + value;
    o2ws_send_cmd("/midiapp/limit", 0, "f", value); 
}

To make this all work, we need to serve the web pages to the browser and forward O2 messages from the microcontroller to MIDI. This is done by running the o2host program and configuring it to forward the midiout service to MIDI (I chose the IAC Driver Bus 1 device). Also, we set HTTP Port to 8080; any value here will enable web services and O2-over-web-sockets. The root for web pages is www:

--------------------------------------------------------------------------
Configuration: acc2midi              Load   Delete 
    Rename to:                       Save   New 

Ensemble name:     rbdapp                            Polling rate: 500 
Debug flags:       d                                 Reference Clock: Y
Networking (up/down to select): local network      
HTTP Port: 8080  Root: www                            
MQTT Host:                                  MQTT Port:      
MIDI Out Service midiout              to IAC Driver Bus 1             (X )

··········································································
New forward O2 to OSC:          New forward OSC to O2:  
New MIDI In to O2:      New MIDI Out from O2:      MIDI Refresh:  
Type ESC to start, Control-H for Help.
--------------------------------------------------------------------------

Be sure to run o2host in the directory containing the www directory so that it can find and serve the web pages.

Conclusions

This post shows how you can use a web page to control parameters in a microcontroller, which normally has no convenient graphical interface. One thing I learned is that Wi-Fi is not a great low-latency way to play MIDI. I experience a lot of delays even with a dedicated router. O2 implements clock sync and in principle you could use “forward synchronous” timing to reduce jitter, but the recovery time for dropped TCP packets also seems very long, so fixing the problem with timestamps would add a lot of delay. Another option is BlueTooth, but my experience with BlueTooth is that it is hard to configure. It certainly does not have the kind of naming and discovery mechanisms of TCP/IP.

O2 supports an approach to building systems in a modular fashion, using multiple application programs with high functionality and controlling them through a flexible messaging system (O2). This illustrated here by splitting the application across a Browser for its graphical interface, a microcontroller because it is hand-held and has many sensing capabilities, and a synthesizer for making sound. O2host serves as a hub that ties it all together, allowing other components to use the lightweight o2lite protocol rather than a full O2 implementation.

Let me know if you need help with O2 or getting this example to work.

Additional references appear in the left sidebar (scroll up).