Skip to content

Instantly share code, notes, and snippets.

@tomoyanonymous
Last active October 13, 2020 12:28
Show Gist options
  • Save tomoyanonymous/5954632487aa0853c203a5cc313cc8bd to your computer and use it in GitHub Desktop.
Save tomoyanonymous/5954632487aa0853c203a5cc313cc8bd to your computer and use it in GitHub Desktop.
How to Run C++ exported from gen~(Cycling'74 Max) on Daisy

Translated with www.DeepL.com/Translator (free version) from Japanese. Original Article https://zenn.dev/tomoyanonymous/articles/c4e9dc8e26f78a2093e8

What is Daisy

https://www.electro-smith.com/daisy It's a microcontroller board that can be developed with Arduino IDE based on STM32 and various other microcontroller boards such as DSP libraries and Eurolac modules.

What is Gen

Cycling'74 Max has a feature that allows you to create low-level signal processing code for use with Cycling'74 Max. The name of the patch is .gendsp instead of .maxpat. As a bonus, there is a function to export to C++, so you can use it for vst signal processing and so on. But I'm a little worried about its future because I've been neglecting it in the development of Cycling recently.

https://cycling74.com/tutorials/gen~-for-beginners-part-1-a-place-to-start

Setting up the Daisy development environment

https://github.com/electro-smith/DaisyWiki/wiki/1a.-Getting-Started-(Arduino-Edition)

Install the Arduino IDE

https://www.arduino.cc/en/Main/software ...

You can also use brew cask arduino.

Put an STM32 board into the Arduino IDE.

https://github.com/stm32duino/wiki/wiki/Getting-Started

Install STM32CubeProgrammer.

↑The part described in the Extra Step above, which is necessary for uploading by DFU

https://www.st.com/en/development-tools/stm32cubeprog.html

If you enter your email address in the "Open" section, a DL link will be sent to you. (In case of Mac, there is .app, but it opens without moving to /Application).

In addition, Java is necessary. It's hard to use Java. You can install Java by brew install openjdk. When you open it, you can see the actual programmer application uploaded to Applications.

Add DaisyDuino library.

https://github.com/electro-smith/DaisyWiki/wiki/1a.-Getting-Started-(Arduino-Edition)#install-the-daisyduino-library

Sketch->Include Library->Manage Library->Run the Library Manager, search for and install DaisyDuino.

How to upload your sketch.

  • Tool->Board and select Generic STM32H7 Series (if you don't have it, you don't have an STM32duino installed)
  • Tool->Board-> part number and select Daisy Seed.
  • Tool-> USB Support(if available) and CDC (generic 'Serial' supersede U(S)ART).
  • In Tool-> USB Speed, use Low/Full Speed for now
  • In Tools->Upload Method, STM32CubeProgrammer (DFU)

Random Notes

One thing that is different from a normal Arduino board is that you don't have to select the USB port in tools->port (or you won't see it as an option). If you don't have it, it will still be uploaded.

Also, Daisy has two buttons on the board of the seed, RESET and BOOT, pressing BOOT and releasing RESET puts the board into write mode and stops the sound, allowing you to write your sketch. If you forget, you'll get an upload error.

Exporting patches made in Gen.

First, create the appropriate .maxpat and .gendsp files in the appropriate folder. This time I pulled the filter code from gendsp examples.

The filter's frequency and Q settings are input, which is a bit difficult to use, so I made a patch that replaces them with params respectively.

After loading this gendsp, create a [gen~ filter.gendsp] object at maxpat and send an exportcode filter.cpp message. The first time, you will be asked for a folder to export to.

Make a folder for Arduino sketches.

Create a .ino file with an appropriate name in an appropriate folder. Insert the code exported by gendsp into this folder.

The structure of the folder is shown in the screenshot below, except for filter.cpp and filter.h. There are some library files in the folder named gen_dsp, which I put together in a single hierarchy.

Tweaking the header file of Gen.

The code generated by this gen automatically conflicts with some parts of the standard library of daisy and stm. First I tried to solve this by modifying preprocessor definition, but I couldn't find a way to solve it, so I had to comment out the code.

Lines 148-157 of genlib_ops.h.

#ifndef WIN32 //comment out here
// inline t_sample exp2(t_sample v) { return pow(2., v); }

// inline t_sample trunc(t_sample v) {
// t_sample epsilon = (v<0.0) * -2 * 1E-9 + 1E-9;
// copy to long so it gets truncated (probably cheaper than floor())
// long val = v + epsilon;
// return val;
// }
#endif // WIN32

Also lines 621~627 of genlib_ops.h.

Depending on the your processing in gendsp, dbtoa or mstosamps above and below this function may also conflict. Please check for errors and try to fix them.

// inline t_sample ftom(t_sample in, t_sample tuning=440.) {
// return t_sample(69. + 17.31234050465299 * log(safediv(in, tuning)));
// }

// inline t_sample mtof(t_sample in, t_sample tuning=440.) {
// return t_sample(tuning * exp(.057762265 * (in - 69.0)));
// }

Lines 153 to 157 of genlib.cpp

// NEED THIS FOR WINDOWS: Comment out here
// void *operator new(size_t size) { return sysmem_newptr(size); }
// void *operator new[](size_t size) { return sysmem_newptr(size); }
// void operator delete(void *p) throw() { sysmem_freeptr(p); }
// void operator delete[](void *p) throw() { sysmem_freeptr(p); }

Now you're ready to go.

Write a sketch.

I have a Daisy Pod, so I looked for some examples for it that I could use, and this time I used the following SimpleOscillator.ino file as a base.

https://github.com/electro-smith/DaisyDuino/blob/master/examples/Pod/SimpleOscillator/SimpleOscillator.ino

Originally, the waveform type can be changed by the encoder, the octave can be changed by the button, and the frequency canbe changed by the knob 1. So I changed it so that knob 1 changes the filter frequency and knob 2 changes the resonance.

#include "DaisyDuino.h"

#include "json.h"
#include "json_builder.h"

#include "genlib.h"
#include "filter.h"

#define NUM_WAVEFORMS 4

DaisyHardware   hw;
Oscillator osc;
CommonState* filter_instance;

uint8_t waveforms[NUM_WAVEFORMS] = {
    Oscillator::WAVE_SIN,
    Oscillator::WAVE_TRI,
    Oscillator::WAVE_POLYBLEP_SAW,
    Oscillator::WAVE_POLYBLEP_SQUARE,
};

static float   freq=1000;
float filterfreq = 1000;
float filterreson = 0.2;
const size_t blocksize = 512;
float** mybuffer;
float          sig;
static int     waveform, octave;

static void AudioCallback(float **in, float **out, size_t size)
{
    hw.DebounceControls();

    waveform += hw.encoder.Increment();
    waveform = (waveform % NUM_WAVEFORMS + NUM_WAVEFORMS ) % NUM_WAVEFORMS;
    osc.SetWaveform(waveforms[waveform]);

    if(hw.buttons[1].RisingEdge())
        octave++;
    if(hw.buttons[0].RisingEdge())
        octave--;

    octave = DSY_CLAMP(octave, 0, 4);

    // convert MIDI to frequency and multiply by octave size
    filterfreq = analogRead(PIN_POD_POT_1) / 1023.f;
   freq = mtof(1000 * 127 + (octave * 12));
    filter::setparameter(filter_instance,0,filterfreq*20000,nullptr);

    filterreson = analogRead(PIN_POD_POT_2) / 1023.f;
    filter::setparameter(filter_instance,1,filterreson,nullptr);
    
    osc.SetFreq(1000);

    // Audio Loop
    for(size_t i = 0; i < size; i ++)
    {
        // Process
        sig        = osc.Process();
        out[0][i] = sig;
        out[1][i] = sig;
    }
    float** out0 =&out[0]; 
    filter::perform(filter_instance,out0,1,mybuffer,1,size);
    for(size_t i = 0; i < size; i ++)
    {
        // Process
        out[0][i] = mybuffer[0][i]*1;
    }
}

void InitSynth(float samplerate)
{
    osc.Init(samplerate);
    osc.SetAmp(0.1);

    waveform = 0;
    octave   = 0;
    freq=1000;
}

void setup()
{
    float samplerate, callback_rate;
    hw = DAISY.init(DAISY_POD, AUDIO_SR_48K);
//    hw.SetAudioBlockSize(blocksize);

    mybuffer = new float*[1];
    mybuffer[0] = new float[blocksize];
    samplerate = DAISY.get_samplerate();
    auto blocksize =  samplerate/DAISY.get_callbackrate();
    filter_instance = (CommonState*)filter::create(samplerate,blocksize);
    hw.leds[0].Set(false,false,false);
    hw.leds[1].Set(false,false,false);

    InitSynth(samplerate);

    DAISY.begin(AudioCallback);
}

void loop()
{
}

Summary of the code above

Include.

#include "json.h"
#include "json_builder.h"
#include "genlib.h"
#include "filter.h"

I put this after DaisyDuino.h. I'm not sure how and when cpp files are compiled and linked, but it seems the dependencies are compiled automatically..

instance creation

//in global
CommonState* filter_instance;


// in setup()
    samplerate = DAISY.get_samplerate();
    auto blocksize = samplerate/DAISY.get_callbackrate();
    filter_instance = (CommonState*)filter::create(samplerate,blocksize);

For some reason Daisy can't get the blocksize directly, but can only receive it by converting it to callbackrate, so I'm starting up an instance of the filter again with the undo process. So, I'm going to bite it and start up the instance again, and the namespace of filter::create should be the same as the name of gendsp.

Parameters.

filter::setparameter(filter_instance,1,filterreson,nullptr);

The second argument specifies the index of the parameters to be set in gendsp when multiple parameters are created. You can use getparametername(filter_instance,index) to get the name of the param, but usually it is easier to directly refer to the definition in the .cpp file.

Implementing Audio Processing.

The daisy audio callback comes with static void AudioCallback(float **in, float **out, size_t size).

filter::perform(filter_instance,out0,1,mybuffer,1,size);

The arguments are, a pointer to the instance, a pointer to the input (float**), the number of input channels, a pointer to the output (float**), the number of output channels, and the buffer size. The buffer size can be specified directly by the size argument of callback.

I thought I could use the same pointers for in and out and do destructive rewriting, but I couldn't, so I prepared a single buffer.

And then, compile and upload it. Finish!

Random Thoughts

  • I'd like to see some more cpp-like code because the API of cpp made with gen~ is mostly C.
  • Why does the headphone out seem to be in mono? I couldn't help it, so I plugged my headphones directly into the lineout (be careful with the volume).
  • Maybe if all the procedure could be automated with node for max or something, you can upload automatically from Max if you want to.
  • But for the time being, it's going to be easier to do it on your own this way until the daisy's get that infrastructure in place.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment