Roger B. Dannenberg

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

Using O2host with O2lite and MicroPython


Summary

O2host provides a simple way to connect browsers and microcontrollers to OSC, MIDI, and other browsers and microcontrollers. This is a guide to this versatile tool that runs on MacOS, Windows, and Linux.

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.

Articles on O2 are listed in my bibliography.

Increasingly, it makes sense to build applications and systems by combining ready-made components. It is great if you have the time and ability to write code based on various libraries, but sometimes, you would like to use complete applications (maybe because they have great user interfaces or maybe because they are just so complicated that it’s best not to deconstruct them). With the ubiquity of networks, particularly Wi-Fi, we can use networks to interconnect software components rather than writing code.

O2 is designed for this approach to software construction and integration. It uses “discovery” to form connections so you do not have to type IP addresses and port numbers to form connections, and it generally makes peer-to-peer connections, giving lower latency than routing through a server.

O2lite offers a subset of of O2 features. Mainly, O2lite connects directly to just one O2 “host” running the full O2 protocol. From there, messages can be relayed to any other O2 process. O2lite runs in browsers, using WebSockets to connect to the host, and on microcontrollers including ESP32 and Raspberry Pi.

O2host is a small application that implements a configurable host process that you can connect to from O2lite. O2host has many applications:

  1. O2host can send and receive Open Sound Control (OSC) messages and MIDI messages, so you can easily relay messages from a microcontroller to various existing devices such as synthesizers and OSC-enabled music software. (This is described in this blog post.)
  2. A web browser can connect to O2host to get access to MIDI, OSC and processes running O2 or O2lite. (See Controlling Soundcool with a Web Browser Using O2host for an example.)
  3. O2lite processes can intercommunicate using O2host as a hub to relay messages, e.g. use a web page as a graphical interface for an embedded microcontroller application. (See O2host and Browser-Microcontroller Communication for an example.)
  4. O2host processes can discover and communicate from anywhere on the internet, so you can use a pair of O2host processes as a world-wide bridge for MIDI, OSC, web browsers, and anything using O2 or O2lite. (See Using O2host as an OSC Relay for an example.)

Let's create some small projects to illustrate each of these possibilities. In this post, I will show how to connect physical controls to a sofware synthesizer using MicroPython running on a microcontroller and connecting using O2 and o2host.

In future posts, I will describe:

Playing a Software Synthesizer from MicroPython using O2host

For this project, I will demonstrate controlling an effect parameter with a physical knob. The data flow is as follows: The knob connects to an ESP32 Thing microcontroller's analog input. The data is read by a small MicroPython program and converted to a number to send to the effect. The number is sent via O2 over Wi-Fi to the o2host program running on my laptop. The o2host program is configured to forward incoming O2 messages to OSC over UDP. The OSC messages go to an effect module running in Soundcool. Here’s a picture:

Reading the Potentiometer

The first task is getting data from the potentiometer (“pot” from now on). I wired the pot as a voltage divider from ground (the pin labeled GND) to 3.3V (the pin labeled 3v3). The variable voltage is connected to the connection labeled 34 (it is not actually pin 34 of the PC board, but it connects to pin 34 of the ESP32 chip on the board). Here's a program in MicroPython:

from machine import Pin, ADC
import time

pot = ADC(Pin(34))
pot.atten(ADC.ATTN_11DB)  # full range: 3.3v
led = Pin(5, Pin.OUT)

while True:
    pot_value = pot.read()
    print(pot_value)
    time.sleep(0.05)

If I put this code in the file adctest.py on my laptop (here is the source), I can run it with ampy (your serial port name will be different):

% ampy --port /dev/tty.usbserial-D306EBLS run adctest.py
  

In the output, you will see the values read from the ADC.

Installing O2lite

I want to use O2lite to send data over Wi-Fi to my laptop. The first step is to install O2lite on my ESP32 microcontroller. Assuming you have already install Micropython, you'll need to install three libraries: micropython-mdns, o2litepy, and globals. Start by connecting to the ESP32 with screen (adapting this to your own serial port name):

screen /dev/tty.usbserial-D306EBLS 115200

Now typing to the ESP32 Micropython, first connect to Wi-Fi:

> import network
> sta_if = network.WLAN(network.STA_IF)
> sta_if.active(True)
> sta_if.connect("Fios-DSCGL", "password")
> import mip
> mip.install("github:cbrand/micropython-mdns")

Continuing on the ESP32, install o2litepy:

> mip.install("github:rbdannenberg/o2/o2litepy")

Finally, to install globals, exit from screen (Control-a, Control-\, y), then:

% echo "# globals.py" > globals.py
% ampy --port YOUR_SERIAL_PORT put src/globals.py globals.py

Note that the module globals is just an empty namespace used to convey the internal Wi-Fi IP address to o2litepy.

Sending Data via O2

Next, we will modify the program to send via O2 over Wi-Fi. In O2, you send to a “service”, so we do not have to worry yet about forming a connection. O2 will take care of it later. Here is the code with the O2 additions in gray background (source is here):

from machine import Pin, ADC
import time
from o2litepy import O2lite

pot = ADC(Pin(34))
pot.atten(ADC.ATTN_11DB)  # full range: 3.3v
led = Pin(5, Pin.OUT)

o2l = O2lite()
o2l.initialize("adctest")  # ensemble name

while True:
    pot_value = pot.read()
    print(pot_value)
    o2l.send("/useadc/1/fader2", 0, "f", pot_value)
    o2l.sleep(0.05)

This code is saved in adc_o2.py.

Notice the use of o2l.sleep instead of time.sleep. This is critical because o2l.sleep calls o2l.poll, which is necessary for O2lite to function. You should call either o2l.sleep or o2l.poll frequently in your program.

An important reason to call o2l.sleep as opposed to o2l.poll in this case is that if you do not do something to limit the rate of messages, it seems that something blocks message sending periodically, so you will experience an occasional long gap between messages. With o2l.sleep to limit the message rate to 20 per second, I measured a maximum interval of about 67 msec between messages and a typical message period of 64 msec. The message rate could be increased, but at some point you will saturate the network or at best start interfering with others sharing the networ. Consider that while Wi-Fi might have a fairly high bandwidth, that is only achieved with fewer and larger messages. Sending many tiny messages like this is relatively inefficient.

Receiving O2 with o2host

Now, we'll see if we can receive the O2 messages using the o2host application. o2host can be downloaded from the O2 project on Github. Run o2host from a terminal.

You will see a form for options like this:

You can get a help screen by typing Control-H:

Type the Escape key to get back to the configuration screen and fill it out as shown above. Here are some details to note:

Quick Test

With o2host configured, we are ready for a quick test. (There is nothing to receive OSC messages, but at least we can see if o2host is receiving O2 messages from the ESP32.)

Be sure to Save_ your configuration. Next time you run o2host, the saved configuration will be loaded automatically.

Now, type the Escape key to o2host to start it. Leave it running and use another terminal window to start adc_o2.py as follows:

Run the adc_o2.py program (see “Sending Data via O2” above). I use the runpy shell script described in a previous blog post, which will prepend Wi-Fi setup code to adc_o2.py and run it on the ESP32. In a terminal application, type:

% uprun adc_o2.py

After 5 or 10 seconds, the ESP32 should find o2host and start sending messages to it. The ESP32 output should look like:

% uprun adc_o2.py
Running make to copy new files to esp32
make: Nothing to be done for `upload'.
make completed
Running main.py with Wi-Fi setup prefix
IP Address 192.168.1.186
1916
1943
1927
...

The o2host program output should look something like:

% ./o2host
------------------------------------------------
You have defined 2 of 20 maximum configurations
Initializing O2 process for ensemble adctest
Polling rate 20
Debug flags: r
Reference clock: Y
Network option: local network
WebSockets: Disable
MQTT: Disable
O2 [L: 6097701.245]: ** new service _o2 is 0x13f808370 (HASH) active 1

==================================================================================
O2 [L: 0.000]: Local Process Name is @00000000:c0a801b6:ca4a (192.168.1.182:51786)
            Receive Port 52185 (0xcbd9)
==================================================================================

O2 [0.001]: obtained clock sync at 0.00111125
O2 [0.001]: ** new service _cs is 0x13f808bd0 (HASH) active 1
O2 [0.001]: ** reference clock established, time is now 0.00113446
O2 to OSC Service useadc via UDP to IP 127.000.000.001 Port 8867
O2 [0.001]: ** new service useadc is 0x13f8099f0 (OSC_UDP_CLIENT) active 1
Configuration complete, running o2host now ... ^C to quit.
------------------------------------------------
O2 [21.258]: msg received (0x13f808800) at 21.2581s (local 21.2581s) by: BRIDGE
    /useadc/1/fader2 @ 0 by TCP 0.474481f
O2 [21.316]: msg received (0x13f808800) at 21.3165s (local 21.3165s) by: BRIDGE
    /useadc/1/fader2 @ 0 by TCP 0.470574f
O2 [21.372]: msg received (0x13f808800) at 21.3717s (local 21.3717s) by: BRIDGE
    /useadc/1/fader2 @ 0 by TCP 0.471306f
O2 [21.431]: msg received (0x13f808800) at 21.4312s (local 21.4312s) by: BRIDGE
    /useadc/1/fader2 @ 0 by TCP 0.469109f
O2 [21.496]: msg received (0x13f808800) at 21.4963s (local 21.4963s) by: BRIDGE
    /useadc/1/fader2 @ 0 by TCP 0.468376f
...

You can see that the ESP32 is printing raw data input values. o2host is receiving these as O2 messages, which are printed as address, timestamp (0), “by TCP” and the single floating point parameter, e.g. 0.474481f, which corresponds to an ADC value (1943) divided by 4095 to normalize it.

Looking carefully, you might deduce that o2host missed the first message (ADC value 1916). When you send to a service that does not exist, O2 simply drops the message, and in this case, the ESP32 had not received any information about service useadc before the first send command. It is possible to check the status of services and wait for them to be established, but since we are sending continuously, it does not matter for this application.

Setting up Soundcool

Soundcool is an easy-to-use modular audio and video processor for creative and collaborative productions. It features control through OSC, which allows multiple participants to control a set of processing modules using phones and tablets running the Soundcool App.

You can also send OSC from a program, and our goal is to connect the potentiometer on the ESP32 to the delay feedback parameter in Soundcool. Here is what the Soundcool patch looks like (the Soundcool patch is here):

Microphone input is processed by a Delay module and sent to the speakers. Notice the Delay port number is 8867, which matches the O2-to-OSC forwarding set up in o2host.

Addresses are also important. The Micropython program adc_o2.py sends data to /useadc/1/fader2. o2host forwards service useadc to OSC port 8867. When forwarding to OSC, the service name is removed from the address, so the OSC address is /1/fader2, which is the address for the feedback parameter. (And by the way, /1/fader1 controls the feedback delay, so you could add additional potentiometers to the ESP32 and control multiple parameters.)

Here is a video of the system in operation:

Debugging

If things are not working, you can turn on some printing to get more information. The o2l.initialize("adctest") call in adc_o2.py can take an optional parameter debug_flags, so if you write o2l.initialize("adctest", debug_flags="a"), then “all” debug options (except the lowest-level “b” option to print message content in binary) are enabled, so you should see if/when o2host is discovered, when messages are sent, etc., displayed in the ESP32 serial (terminal) output.

Summary

This post shows how you can use O2lite and Micropython to connect a microcontroller (ESP32) via Wi-Fi to a software synthesizer running on a laptop. The o2host program is used as an intermediary.

The microcontroller initializes O2lite which automatically “discovers” the o2host program running on the laptop. o2host is configured to forward O2 messages to OSC, and the synthesizer (Soundcool) is configured to listen for OSC messages. Using o2host as a “gateway” eliminates the need to write code to receive and forward messages.

These same techniques can be used to for many types of sensors controlling many different types of programs, as long as they support O2, OSC, or MIDI protocols. The o2litepy module should run in Micropython on a variety of microcontrollers with Wi-Fi or Ethernet.

In the next post, I will show how you can use HTML and Javascript in a web browser to communicate through O2, OSC and MIDI.

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