Learning to code for Ornament and Crime Hemisphere suite

Some notes on coding for the open-source eurorack module called Ornament and Crime
Author

Matt Crump

Published

July 28, 2020

Last knit: 2023-08-04

I don’t really blog about synthesizers, but I like them, and I’m slowly building a modular eurorack system. I’ve been playing around with Ornament and Crime, and then recently installed the alternative firmware hemisphere suite by Chysn. Both firmwares are very good. I forked the Hemisphere repo, and am now trying to code my own applets using the hemisphere approach. Using this page to make some notes as I go along.

I got started messing with O_C yesterday, and made some decent progress (although I don’t really know C++, so it’s slow going).

I made a copy of the hemisphere source code and am modifying it in my own github repo https://github.com/CrumpLab/O_C-MINERVA. My basic plan is twofold. My overall goal is to start programming essentially weird versions of shift-register/turing machine style sequencers. Ultimately, I have plans to implement some computational models of human memory as eurorack sequencers, in particular Minerva II (Hintzman, 1984, 86, 88). But, to get there I need to learn how to do some more basic stuff.

My plan for learning is to systematically re-write my own version of the hemisphere suite to figure out what’s going on, and then hopefully learn enough to start messing with it in musically interesting ways. The rewrite will be way less interesting than what is there already, and will focus on me learning basic operations.

Getting Started

It took about 2-3 hours before I was able to verify that I could modify the source code, compile it, and upload it to the unit and see that the pipeline was working. It should have been about 5-10 minutes, but I got stuck on stupid stuff. The instructions are clearly laid out on the original O_C site under method B for compiling firmware https://ornament-and-cri.me/firmware/#method_b. There are two methods, and they both word for installing the original or alternative firmwares. Method A involves loading a pre-compiled hex file. Method B compiles from source and then loads. I’m using method B, and in terms of development it works pretty seamlessly (e.g., you can keep the O_C hooked up by usb, and do one-click compile and upload whenever you want).

You need three things (links to software on method b link above)

  1. download the arduino IDE 1.8.1
  2. download Teensyduino v1.35
  3. have a micro-usb cable to connect your computer to the O_C

First install the Arduino IDE, then teensyduino, which will ask to install into your arduino app. NOTE: This is where I stupidly ran in a bunch of problems. I confused arduino IDE 1.8.10 for 1.8.1, and spent an hour or two trying to figure out why Teensyduino 1.35 (which is made for Arduino 1.8.1, NOT 1.8.10) wasn’t installing. Yeesh. Anyway, I got it to install just fine once I had the correct versions (I’m on a Mac 10.14.6).

Compiling and uploading

Step 2 of Method B is to have a copy of the firmware that you want to change. My impression is that the alternative firmware hemisphere suite will be easier for me to learn from, so I’m using that. I downloaded the source from github. Then you open o_c_REV.ino (in the software folder) in the arduino app.

The next step is to set some preferences prior to compiling:

  1. select teensy 3.2/3.1 in Tools > Board. and
  2. select MIDI > USB Type (This is different from the original instructions, the hemisphere suite adds some midi functionality through USB, and it won’t compile properly without this setting) . and
  3. select 120MHz optimized (overclock) in Tools > CPU Speed. and
  4. select Faster (= o2) in Tools > Optimize (teensyduino 1.34 and 1.35)

Finally, you need your O_C powered on through the eurorack power source, and you need to be connected to your computer by USB, then you can press sketch > upload in the Arduino app. This should compile then upload and restart the O_C. This process takes about 20 seconds or less usually.

General observations

I’m taking a combination of staring at the code and poking around various websites to try and figure out how everything works. I haven’t found any clear documentation anywhere, but I could have easily missed it. The code base is really well thought out, so it almost reads like an instruction manual.

Chysn blogged about developing hemisphere suite, and there are numerous helpful tidbits there. He also has another repository with some tutorials on programming O_C firmware https://github.com/Chysn/O_C_Tutorial1. There is a support forum for hemishpere here https://llllllll.co/t/hemisphere-suite-support/21704.

When you load the hemisphere suite firmware, you will get a bunch of “apps”, including the hemisphere suite. For example, some of the apps are the game Pong, the darkest timeline (cool sequencer), ENIGMA (Turing machine variant), others, and hemisphere.

So, depending on what you want to do, you could write another “app” that gets added to this list and/or you could write additional modules/applets for the hemisphere suite (there are memory issues, so have to delete something first). Right now I’m only focusing on modules for the hemisphere suite.

Hemisphere splits the O_C into two sides, and each side can load pretty much any of the applets. So, you could have the BootsNCats module loaded on one side to make kicks and snares, and a two channel euclidean sequencer on the other side controlling the kick and snare. Hemisphere has something like 51 different modules that can be loaded. Each side of hemisphere has one button, one endless encoder (with push), two gate inputs, two cv inputs, and two cv outputs, and half of a small screen for graphics.

Brief code overview

Chysn provides the basic steps to write a new hemisphere module in software/HEM_Boilerplate.ino.txt, copied below. The short story is that all of the applets in hemisphere are listed as HEM_Classname.ino files in software/o_c_REV. You can add your own new ones, list them in hemisphere_config.h and incremement HEMISPHERE_AVAILABLE_APPLETS to the new number of total apps. Then compile and upload. There are memory limitations, and this suite is already packed to the brim, so delete stuff as necessary. I’m super duper happy with the template for coding an applet. This one file takes care of everything, including the code for the display.

// Hemisphere Applet Boilerplate. Follow these steps to add a Hemisphere app:
//
// (1) Save this file as HEM_ClassName.ino
// (2) Find and replace "ClassName" with the name of your Applet class
// (3) Implement all of the public methods below
// (4) Add text to the help section below in SetHelp()
// (5) Add a declare line in hemisphere_config.h, which looks like this:
//     DECLARE_APPLET(id, categories, ClassName), \
// (6) Increment HEMISPHERE_AVAILABLE_APPLETS in hemisphere_config.h
// (7) Add your name and any additional copyright info to the block below

// Copyright (c) 2018, __________
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

class ClassName : public HemisphereApplet {
public:

    const char* applet_name() { // Maximum 10 characters
        return "ClassName";
    }

    /* Run when the Applet is selected */
    void Start() {
    }

    /* Run during the interrupt service routine, 16667 times per second */
    void Controller() {
    }

    /* Draw the screen */
    void View() {
        gfxHeader(applet_name());
        gfxSkyline();
        // Add other view code as private methods
    }

    /* Called when the encoder button for this hemisphere is pressed */
    void OnButtonPress() {
    }

    /* Called when the encoder for this hemisphere is rotated
     * direction 1 is clockwise
     * direction -1 is counterclockwise
     */
    void OnEncoderMove(int direction) {
    }
        
    /* Each applet may save up to 32 bits of data. When data is requested from
     * the manager, OnDataRequest() packs it up (see HemisphereApplet::Pack()) and
     * returns it.
     */
    uint32_t OnDataRequest() {
        uint32_t data = 0;
        // example: pack property_name at bit 0, with size of 8 bits
        // Pack(data, PackLocation {0,8}, property_name); 
        return data;
    }

    /* When the applet is restored (from power-down state, etc.), the manager may
     * send data to the applet via OnDataReceive(). The applet should take the data
     * and unpack it (see HemisphereApplet::Unpack()) into zero or more of the applet's
     * properties.
     */
    void OnDataReceive(uint32_t data) {
        // example: unpack value at bit 0 with size of 8 bits to property_name
        // property_name = Unpack(data, PackLocation {0,8}); 
    }

protected:
    /* Set help text. Each help section can have up to 18 characters. Be concise! */
    void SetHelp() {
        //                               "------------------" <-- Size Guide
        help[HEMISPHERE_HELP_DIGITALS] = "Digital in help";
        help[HEMISPHERE_HELP_CVS]      = "CV in help";
        help[HEMISPHERE_HELP_OUTS]     = "Out help";
        help[HEMISPHERE_HELP_ENCODER]  = "123456789012345678";
        //                               "------------------" <-- Size Guide
    }
    
private:

};


////////////////////////////////////////////////////////////////////////////////
//// Hemisphere Applet Functions
///
///  Once you run the find-and-replace to make these refer to ClassName,
///  it's usually not necessary to do anything with these functions. You
///  should prefer to handle things in the HemisphereApplet child class
///  above.
////////////////////////////////////////////////////////////////////////////////
ClassName ClassName_instance[2];

void ClassName_Start(bool hemisphere) {ClassName_instance[hemisphere].BaseStart(hemisphere);}
void ClassName_Controller(bool hemisphere, bool forwarding) {ClassName_instance[hemisphere].BaseController(forwarding);}
void ClassName_View(bool hemisphere) {ClassName_instance[hemisphere].BaseView();}
void ClassName_OnButtonPress(bool hemisphere) {ClassName_instance[hemisphere].OnButtonPress();}
void ClassName_OnEncoderMove(bool hemisphere, int direction) {ClassName_instance[hemisphere].OnEncoderMove(direction);}
void ClassName_ToggleHelpScreen(bool hemisphere) {ClassName_instance[hemisphere].HelpScreen();}
uint32_t ClassName_OnDataRequest(bool hemisphere) {return ClassName_instance[hemisphere].OnDataRequest();}
void ClassName_OnDataReceive(bool hemisphere, uint32_t data) {ClassName_instance[hemisphere].OnDataReceive(data);}

First attempts at coding

Change one thing to make sure it works

The first thing I did was try to make single change in the source that I could verify would compile correctly and not brick the O_C. I went into HEM_BootsNCat.ino and modified line 29-30 to change the t in Boots to a p.

    const char* applet_name() {
        return "BoopsNCat";
    }

Sure, enough, I press upload, scroll to that applet, and voila, it worked, and suddenly I feel like Jeff Bridges from TRON (the recent one where he says he “got in” in that Daft Punk song).

Fiddle around a bunch

After changing a single letter, I then started changing numbers and a few other things, while also scanning through the code to see how it was connected.

Some of the problems that I ran into right away were questions about the DAC (digital analog converter) and ADC (analog digital converter). The input voltages are digitized, transformed if necessary, and then converted back to analog and sent out. The issue is knowing what range of values you are dealing once they are digitized so you know what kind of math to apply to the signals. Fortunately, a lot of these details are sorted out in the hemisphere and original o_c code base, it just takes some time to figure everything out. This post was helpful for me to think about the conversion issues http://butmostlycrime.blogspot.com/2018/09/pitch-calculation-and-output.html. But, I still think I need to understand these issues more clearly.

Also, it’s worth reading the specs on the unit https://ornament-and-cri.me/hardware-basics/.

Some additional poking around helped me understand some of the structure of hemisphere suite. For example, in hemisphere there is a function In() that will read in cv values from the 0 or 1 (e.g., In(0)) channel. This function is defined in the Offset I/O methods section of HemisphereApplet.h. This file further defines other general hemisphere functions for ins and outs, some which are borrowed from the original firmware codebase. For example, the OC::DAC::set_pitch() function is defined in OC_DAC.h. Also, an inspection of software/o_c_Rev will show there are lots of OC files and various others that are recruited as libraries. So, lot’s of great stuff to work with. But, I haven’t seen any documentation about this, beyond the source code itself.

Next steps

Right now my plan is to learn hemisphere essentially by deleting it re-writing it…up to a point. I need to learn super basic stuff like what values I get when I read in inputs, what values I get when I look at the encoder data, what values I need to send out CV in desired ways…how to deal with clock sinking…all the stuff basically. The hemisphere is already well suited to learning from examples, and all I am doing at the moment is copying and modifying existing applets to try and work out what does what.

To be a little bit more programmatic about my learning, and perhaps even be useful to other people wanted to try this out I’ve started a copy of hemisphere called O_C-MINERVA https://github.com/CrumpLab/O_C-MINERVA. My goal is to make a set of really basic applets that serve as working examples of really basic stuff. These applets won’t necessarilly be useful for eurorack purposes, but they should be useful as examples for me to return to when I want to start to doing more complicated things. I often tell my students when they are learning to program that they should go through the motions of programming functions for common statistics like the mean, or standard deviation. It’s a good way to learn about details. So, I will take my own advice and try to program basic applets and see what happens.

Basic Applets

PrintCV

The first applet I made was PrintCV. The goal here was to make a copy of the boilerplate software/HEM_Boilerplate.ino.txt, rename everything to PrintCV, and then figure out how to do two things.

  1. Pass CV from the CV in of a channel to CV out of a channel

  2. Take the digitized CV input value and print it to the screen

This little applet is at https://github.com/CrumpLab/O_C-MINERVA/blob/master/software/o_c_REV/HEM_PrintCV.ino

I’ll point out a few pieces. First, passing CV from in to out happens in the Controller function.

    /* Run during the interrupt service routine, 16667 times per second */
    void Controller() {
      ForEachChannel(ch) {  /* Sends CV Ins to Outs */
        Out(ch, In(ch));
      }
    }

Note, that ForEachChannel() is defined in HemisphereApplet.h as

#define ForEachChannel(ch) for(int ch = 0; ch < 2; ch++)

which gives a convenient way to loop over channel one and two. Out() and In() are also defined in HemisphereApplet.h. In this case the ch variable just cycles between 0 and 1, covering both the first and second channel. The Controller() function runs at 16667 time per second, and this is the heart where we apply transformations to the digitized signal from the inputs, and send them back to outputs. I’ll note that the above code is sufficient to allow CV inputs 0 and 1 to pass their signal (unmodified) to the outputs 0 and 1.

The other thing I wanted to do was print the value of the incoming CV to the screen. This would help me get a sense of the internal values that I’m working with, and also, a PrintCV function could be a useful way to measure actual CV values, especially offset values that are often static (e.g., precision adder). This part is taken care of in two places:

    /* Draw the screen */
    void View() {
        gfxHeader(applet_name());
        DrawInterface();
        // Add other view code as private methods
    }

The DrawInterface() function is defined later in the private methods, there are several existing gfx...() functions for drawing to the screen, some of them located in the Offset graphics methods of HemisphereApplet.h.

private:

  void DrawInterface() {
    gfxPrint(1, 15, "1: ");
    gfxPrintVoltage(In(0));
    gfxPrint(1, 25, "2: ");
    gfxPrintVoltage(In(1));
  }

I use gfxPrint() to print character values on different lines (controlled by the second y-value, 15, 25, goes down the screen). The gfxPrintVoltage() function will convert the digitized internal value of a current cv channel input to a cv value ranging between -3 and 5 volts (need to double-check this, it’s supposed to be the range of what the O_C hardware is capable of outputting).

CVOffset

The point of CVOffset was to figure out how to use the rotary encoder to control a cv out voltage. As of right now this applet works, but doesn’t prevent itself from doing things that might cause problems.

  1. Assign some starting value of 0 that will be sent out when the applet loads
    /* Run when the Applet is selected */
    void Start() {
      digital_cv = 0;
    }
  1. Here we constantly send out the value in send_cv, which is defined in private
    /* Run during the interrupt service routine, 16667 times per second */
    void Controller() {
      Out(0,send_cv);
    }
  1. OnEncoderMove() returns 1 or -1 depending on direction, so I increment or decrement digital_cv on each turn. Then I use the Proportion() function from HemisphereApplet.h (which also defines HEMISPHERE_MAX_CV), to scale a value in terms of 0 to 100, to a voltage. O_C only outputs -3 to 5 so this function doesn’t attempt to control going over or under.
    /* Called when the encoder for this hemisphere is rotated
     * direction 1 is clockwise
     * direction -1 is counterclockwise
     */
    void OnEncoderMove(int direction) {
      /* output range is -3V and 6V -4608 ~ 9216*/
      if(direction == 1) {
        digital_cv++;
      } else {
        digital_cv--;
      }
      send_cv = Proportion(digital_cv, 100, HEMISPHERE_MAX_CV);
    }
  1. I draw some stuff on the screen to see some values and get a sense of the transformations.
private:

  int digital_cv;
  int send_cv;

  void DrawInterface() {
    gfxPrint(1, 15, "1: ");
    gfxPrint(1, 25, digital_cv);
    gfxPrint(1, 35, send_cv);
    gfxPrint(1, 45, HEMISPHERE_MAX_CV);
  }
};

LofiAudio

The specs suggest that OC can output around 16bit, but it is effectively sampling input at around 1khz. This means that if I send an audio source to a cv in, and listen to the output (e.g., I could use PrintCV which has passes in to out) then I should hear low-fi audio…And, yup that works, super fun. I’m going to use this to make a super simple lofi audio applet, now HEM_LofiAudio.ino. I think I’ll leave this, and mention that hemisphere does some audio input processing examples with the drcrusher and lofitape applets.

Attenuate

Hemisphere has AttenuateOffset already. But, I should learn how to make just a plain and simple attenuator. E.g., Take CV in, attenuate it by an encoder value, and send the attenuated value out. I can copy PrintCV and make Attenuate from there using some of the AttenuateOffset code. OK, this works as an attenuator.

    /* Run when the Applet is selected */
    void Start() {
      ForEachChannel(ch) level[ch] = 63;
    }

    /* Run during the interrupt service routine, 16667 times per second */
    void Controller() {
      ForEachChannel(ch)
      {
          int signal = Proportion(level[ch], 63, In(ch));
          signal = constrain(signal, -HEMISPHERE_3V_CV, HEMISPHERE_MAX_CV);
          Out(ch, signal);
      }
    }

    /* Draw the screen */
    void View() {
        gfxHeader(applet_name());
        DrawInterface();
        // Add other view code as private methods
    }

    /* Called when the encoder button for this hemisphere is pressed */
    void OnButtonPress() {
    }

    /* Called when the encoder for this hemisphere is rotated
     * direction 1 is clockwise
     * direction -1 is counterclockwise
     */
    void OnEncoderMove(int direction) {
      ForEachChannel(ch) {
        level[ch] = constrain(level[ch] + direction, 0, 63);
      }
    }

The level[2] variable is defined later in private. We find the incredibly useful Proportion() and constrain() functions helping us keep cv values in the ranges we need them in. This basic attenuator shows the math, but doesn’t do anything fancy in terms of channel control. There are two channels here, but the encoder value applies the same amount of attenuation to both channels. To implement separate control of each I would look at the hemisphere AttenuateOffset example.

SimpleVCA

Hemisphere already has a VCA algorithm called GatedVCA. I haven’t looked at that yet, and am about to. I thought I should learn how to program a VCA. The point will be to have the level of CV 1 control attenuate a CV 2 input for some output, say output 2. Actually before I do this I should make an attenuator…OK did that above. Onto VCA. Let’s modify by copying Attenuate, and working in some code from GatedVCA.

This is pretty much all you need for a simple VCA. CV 1 is a signal input. CV 2 is another CV whose value will control attenuation of CV1, the attenuated output is out 2 (send channel is 1, first is 0). Works like a charm.

    /* Run during the interrupt service routine, 16667 times per second */
    void Controller() {
          int signal = In(0);
          int amplitude = In(1);
          int output = ProportionCV(amplitude, signal);
          output = constrain(output, -HEMISPHERE_MAX_CV, HEMISPHERE_MAX_CV);
          Out(1, output);
    }

SimpleVCO

The outputs are 16 bit I think, and can support some VCO action. The original OC had the bytebeat algorithm which sounded really cool. I should try and make some kind of simple VCO with V/Oct pitch control…OK, tried messing with this, and haven’t completed the task. The VectorLFO applet can go into the audio range. BootsNCats shows another example.

functions