USB driver for SourceAudio C4 Synth pedal

This is one of the most amazing things I’ve seen!
Thank you.

Note that the video does include an added audio adapter board, which isn't actually necessary if only the C4 Synth functionality is desired. The rotary encoder also has turn indents that prevent slippage.
 
Thank you.

Note that the video does include an added audio adapter board, which isn't actually necessary if only the C4 Synth functionality is desired. The rotary encoder also has turn indents that prevent slippage.
I love the idea of a screen with the names, a detected rotary encoder to select patch (and maybe a second to control the output volume)
I shared the video you had made on a uk based bass forum and a few of us got very excited!

I’m new to a lot of this and need to learn more how to program screens and make it work!
 
I love the idea of a screen with the names, a detected rotary encoder to select patch (and maybe a second to control the output volume)
I shared the video you had made on a uk based bass forum and a few of us got very excited!

I’m new to a lot of this and need to learn more how to program screens and make it work!
This morning I saw a new domain, the bass forum, had linked to the github source tree, and then in turn I came across that thread. And you're correct, I'm in Belfast, Ireland.

I'll throw together simplified version without the audio board, but with another rotary encoder (volume) and say, a 480x320 capacitive touch display. This should allow people to flick through and/or edit the patches, in real-time.
 
This morning I saw a new domain, the bass forum, had linked to the github source tree, and then in turn I came across that thread. And you're correct, I'm in Belfast, Ireland.

I'll throw together simplified version without the audio board, but with another rotary encoder (volume) and say, a 480x320 capacitive touch display. This should allow people to flick through and/or edit the patches, in real-time.
That sounds cool - how would you use the touch screen?


I was thinking of trying to work out a tiny 128x64 screen and try and fit it in as small an enclosure as possible! I’m probably just trying to solve my bug bears with the pedal in that it’s super easy to make sounds that sound good, and to have *enough* control live with the 4 knobs … and I can’t imagine it without a switcher… but I can’t ever remember what the presets are! So it’s preset name and master volume that would be cool to have.
I’m now going into designer mode (the day job) and trying to work out how control functions could work … trying to run before I can walk!
 
That sounds cool - how would you use the touch screen?


I was thinking of trying to work out a tiny 128x64 screen and try and fit it in as small an enclosure as possible! I’m probably just trying to solve my bug bears with the pedal in that it’s super easy to make sounds that sound good, and to have *enough* control live with the 4 knobs … and I can’t imagine it without a switcher… but I can’t ever remember what the presets are! So it’s preset name and master volume that would be cool to have.
I’m now going into designer mode (the day job) and trying to work out how control functions could work … trying to run before I can walk!
Touch input could be used to directly modified multiple onscreen controls without having first select focus. Say, a Bargraph. But could also be used in conjunction with physical input control(s) (keys/knob) for the best of both worlds.

If going with multiple rotary encoders, then I would consider implementing an dynamic encoder input configuration, loadable at runtime from SD. For example, config.cfg, which would index each control with its assigned function.
 
Hey Folks

Many thanks to MichaelMC for sharing his work:)

My intention is to build a controller for the c4 with 16ish encoders to modify parameters (depth, frequency, resonance etc) in real time and have them displayed on a LCD (240x320 ili9341spi). When a new preset is selected the parameters would be updated on the lcd.

I have the Source Audio hub connected to the c4 via the 3.5mm "control port" for preset changes and preset saving/management so I don't need those features from the teensy.


My guess is something like the following:

scan of the c4 to ascertain which preset is selected

use util_listCtrlValues when a new preset is selected via the hub.
Store the values so the encoders know where they are and to update the values of the parameters on the lcd.

use util_setCtrlValue to send the values to the c4 when the encoder is moved.
each encoder would be would be tied to a parameter

use util_getCtrlValue to get the updated value


Does anyone have any suggestion or guidance on how to approach this? Should I use the default teensy encoder library or an alternate?
Any insight would be appreciated or perhaps even an example for one parameter/encoder I can extrapolate (I'm much more a musician than programmer)

Thanks
 
Last edited:
Hey Folks

Many thanks to MichaelMC for sharing his work:)

My intention is to build a controller for the c4 with 16ish encoders to modify parameters (depth, frequency, resonance etc) in real time and have them displayed on a LCD (240x320 ili9341spi). When a new preset is selected the parameters would be updated on the lcd.



Does anyone have any suggestion or guidance on how to approach this? Should I use the default teensy encoder library or an alternate?
Any insight would be appreciated or perhaps even an example for one parameter/encoder I can extrapolate (I'm much more a musician than programmer)

Thanks
I would recommend against the 3.5mm interface as would unnecessarily complicate your design. For example, how could you infer a preset change where it happens external to your software? Polling is one option but not recommended to continuously poll the C4. It has other things to do.

For the encoder question, I threw together a small example of using multiple encoders (3), but using a modified version of the encoder library (added a callback from the isr) included. This is based around a minimalist UI framework which should provide a starting point to build off of.
Attached example is ready to compile as is.
 

Attachments

  • encodertest.zip
    14.3 KB · Views: 295
Thanks for your reply MichaelMC.

I did consider the extra complexity the 3.5mm interface adds, I thought I could maybe get away with polling the c4 at a long interval or maybe add a serial midi in port to connect my current midi controller and have the teensy do preset changes also.

Thanks so much for the code, i'll take a look :)
 
Thanks for your reply MichaelMC.

I did consider the extra complexity the 3.5mm interface adds, I thought I could maybe get away with polling the c4 at a long interval or maybe add a serial midi in port to connect my current midi controller and have the teensy do preset changes also.

Thanks so much for the code, i'll take a look :)
The EncoderTool from MatrixRat above looks promising, and is probably better suited to the task, though I've not tried.

Regarding using util_getCtrlValue(). I wouldn't recommend a user interface design where a read back is perform after each and every adjustment (write). Consider using a delayed approach where, say, after 2 seconds, the read-back is performed after last adjustment occurs. Not immediately. This will help with (prevent) bargraph jumping.

Something also to consider; How many parts (levels) per bargraph per control should there be. Have a look at ctrl_c4.c then controls_c4[]. The forth column states how wide, in bits, each control is. Eg; A control at 2 bits wide indicates a bargraph of 4 levels. 1bit would be either 2 or on/off.
 
The EncoderTool from MatrixRat above looks promising, and is probably better suited to the task, though I've not tried.

Regarding using util_getCtrlValue(). I wouldn't recommend a user interface design where a read back is perform after each and every adjustment (write). Consider using a delayed approach where, say, after 2 seconds, the read-back is performed after last adjustment occurs. Not immediately. This will help with (prevent) bargraph jumping.

Something also to consider; How many parts (levels) per bargraph per control should there be. Have a look at ctrl_c4.c then controls_c4[]. The forth column states how wide, in bits, each control is. Eg; A control at 2 bits wide indicates a bargraph of 4 levels. 1bit would be either 2 or on/off.
I'll take a look at the Encoder tool, thanks MatrixRat.
In regards to using getCtrlValue() I had thought the same thing, a longish interval of perhaps 2 seconds between read-back.
In regards to bargraph, I assume you mean a graph to display the value on the lcd? I would prefer a numerical display 0-254 but a bargraph could be good for touch input if you choose to implement that.
 
Last edited:
So I finally got some time over the weekend to do some testing. I can confirm that the code that MichaelMC created does indeed work.
Absolutely amazing work Michael! Thanks so much for sharing - I have a very vague concept of how it is is you managed to reverse engineer this through sniffing the communication but sheesh Massive respect to be able to pull that off!

I've only tested a few functions but so far the C4 pedal does indeed react as intended!
Here is the simple sketch I've made to test functionality

Code:
#include <Arduino.h>
#include "usbh_common.h"
#include "sa_c4.h"
#include "SPI.h"
#include "ILI9341_t3.h"
#include "font_Arial.h"
#include <Encoder.h>

// For the Adafruit shield, these are the default.
#define TFT_DC  9
#define TFT_CS 10
// Use hardware SPI (on Uno, #13, #12, #11) and the above for CS/DC
ILI9341_t3 tft = ILI9341_t3(TFT_CS, TFT_DC);

Encoder myEnc(18, 19);

// Important! Do not access device until this has been set
static int deviceReady = 0;

int driver_callback (uint32_t msg, intptr_t *value1, uint32_t value2)
{
    if (msg == USBD_MSG_DEVICEREADY){
        printf("\r\n - C4 Synth ready - \r\n\r\n");
        deviceReady = 1;
    }
    return 1;
}

void setup ()
{
  //////////////////////////////////////////////////////////
  pinMode(4, INPUT_PULLUP);
  tft.begin();
  tft.fillScreen(ILI9341_BLACK);
  tft.setRotation(2);
  /////////////////////////////////////////////////////////

    Serial.begin(2000000);
    //while (!Serial); // wait for Arduino Serial Monitor                                                       
    printf("Source Audio C4 demo\r\n");
 
    usb_start(AS_PID_C4SYNTH);
    SaC4_setCallbackFunc(driver_callback);
}

long oldPosition  = -999;




void loop ()

{
  long newPosition = myEnc.read();
  long myPosition = newPosition / 4;
  if (newPosition != oldPosition) {
    oldPosition = newPosition;
    if (newPosition == myPosition * 4) {
      Serial.println(newPosition);
 

    if(myPosition > 254)
        myPosition = 254;
    if(myPosition < 0)
        myPosition = 0;
 
 
    tft.setCursor(0, 100);
    tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);  tft.setTextSize(3);  //white text with black background
    tft.println(myPosition);

    util_setCtrlValue("output", myPosition);      ///////////////////////////////////////
 
    }
 
  }
 

 
 
  if (digitalRead(4) == HIGH)
  {
  //Serial.println("Button not pressed!!!");
  }
  else
  {
  tft.setCursor(0, 0);
  tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);  tft.setTextSize(3);  //white text with black background
  tft.println("Hello World!");
  tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);  tft.setTextSize(3);  //white text with black background
  tft.println(newPosition / 4);

  //util_setCtrlValue("output", myPosition);   ///////////////////////////////////////

  }






    usb_task();
 
    if (!deviceReady){
        delay(1);
        return;
    }
 
    delay(30);
}






I should mention that if I dont have the C4 connected to the teensy via USB host cable, the teensy will freeze up and reset after a few seconds.

As a first test I used util_setCtrlValue to change the output level. When using the push button I can successfully send the output value that the encoder has last read. The C4 reacts and changes the volume :)

The prior chunk of code also using util_setCtrlValue("output", myPosition); seems to work ok, when rotating the encoder the output level on the C4 changes in real time:) (worth noting the teensy does one reset cycle but then stays on and works which seems odd?)
More testing needed but its a very promising start - hopefully I can extrapolate this out to 15 or so encoders and it still works..... I will turn only one encoder at any given time so hopefully not an issue.

As far as recalling values after chaging presets via the source audio hub, I think the easiest option is to have a push button that I would need to press when a new preset is selected. It def not ideal but dont actually mind too much if the screen doesnt update automatically, I dont need the screen for preset names though I'm sure others would find that useful. I'll play around with a method that updates the values periodically (maybe every 2-3 seconds) and see if that works.

Also the code I used to have the encoders have limits at 0 and 254 works but is flawed. If I go past 254 by lets say 10 more steps, the value will stay at 254, cool. But if I then rotate back, it takes 10 steps before the value will begin to change.
 
Last edited:
Also the code I used to have the encoders have limits at 0 and 254 works but is flawed. If I go past 254 by lets say 10 more steps, the value will stay at 254, cool. But if I then rotate back, it takes 10 steps before the value will begin to change.
It might be worth using the encoder value to calculate a delta, rather than trying to use the absolute value.
 
I should mention that if I dont have the C4 connected to the teensy via USB host cable, the teensy will freeze up and reset after a few seconds.

The code is writing to the pedal before it is signalled ready, hence the reset. In your code above, ensure 'deviceReady' is checked before calling util_setCtrlValue().
 
Thanks for your replies. I've got 2 encoders working fine and added the check for "deviceReady" to stop the crashing.
Near the end of the code I have tried to add a push button that will update the values of the currently selected preset. For the case of output level, I tried to create an int and have it equal util_getCtrlValue("output"); but the result was a huge number that did not change. My naive guess is this large number is a numeric representation of the first chunk of data returned from util_getCtrlValue("output"); which is text rather than the parameter value?

Can anyone suggest how I should go about retrieving the util_getCtrlValue("output"); value and then update the "newPosition" to equal this retrieved value. This should update the LCD accordingly.

I suppose it would make sense to instead utilize util_listCtrlValues(); to read all values at once and be able to use those values to update the values of the corresponding encoder but I figured the former method is easier for me to understand and implement.



Code:
#include <Arduino.h>
#include "usbh_common.h"
#include "sa_c4.h"
#include "SPI.h"
#include "ILI9341_t3.h"
#include "font_Arial.h"
#include <Encoder.h>

// For the Adafruit shield, these are the default.
#define TFT_DC  9
#define TFT_CS 10
// Use hardware SPI (on Uno, #13, #12, #11) and the above for CS/DC
ILI9341_t3 tft = ILI9341_t3(TFT_CS, TFT_DC);

Encoder myEnc(18, 19);  //encoder 1
Encoder myEnc2(20, 21); // encoder 2

// Important! Do not access device until this has been set
static int deviceReady = 0;

int driver_callback (uint32_t msg, intptr_t *value1, uint32_t value2)
{
    if (msg == USBD_MSG_DEVICEREADY){
        printf("\r\n - C4 Synth ready - \r\n\r\n");
        deviceReady = 1;
    }
    return 1;
}

void setup ()
{
  //////////////////////////////////////////////////////////
  pinMode(4, INPUT_PULLUP);
  tft.begin();
  tft.fillScreen(ILI9341_BLACK);
  tft.setRotation(2);
  /////////////////////////////////////////////////////////

    Serial.begin(2000000);
    //while (!Serial); // wait for Arduino Serial Monitor                                                            wont run without opening serial monitor
    printf("Source Audio C4 demo\r\n");

    usb_start(AS_PID_C4SYNTH);
    SaC4_setCallbackFunc(driver_callback);
}

long oldPosition  = -999;   // encoder 1
long oldPosition2  = -999;  // encoder 2


void loop ()

{ 
  usb_task();

  if (!deviceReady){
        delay(10);
        return;
    }
    else if (deviceReady == 1)
{

  long newPosition = myEnc.read();       //encoder 1
  long myPosition = newPosition / 4;     //encoder 1

  long newPosition2 = myEnc2.read();      // encoder 2
  long myPosition2 = newPosition2 / 4;   // encoder 2




  // encoder 1

  if (newPosition != oldPosition)
  {
    oldPosition = newPosition;
    if (newPosition == myPosition * 4)
    {
      Serial.println(newPosition);
    tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);  tft.setTextSize(3);  //white text with black background

    if(myPosition > 254)
        myPosition = 254;
    if(myPosition < 0)
        myPosition = 0;

    util_setCtrlValue("output", myPosition);     
    tft.setCursor(0, 100);   

    if (myPosition < 10)
      {
       tft.print("   ");
       tft.setCursor(0, 100);   
       tft.print(myPosition); 
      }
    else if (myPosition >= 10)
      {
       tft.print("   ");
       tft.setCursor(0, 100);   
       tft.print(myPosition);
      }
    }   
  }




// encoder 2

  if (newPosition2 != oldPosition2)       
  {
    oldPosition2 = newPosition2;           
    if (newPosition2 == myPosition2 * 4) 
    {
      //Serial.println(newPosition);
    tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);  tft.setTextSize(3); 

    if(myPosition2 > 254) 
        myPosition2 = 254; 
    if(myPosition2 < 0)     
        myPosition2 = 0;   

    util_setCtrlValue("master_depth", myPosition2);     
    tft.setCursor(0, 150);   

    if (myPosition2 < 10)       
      {
       tft.print("   ");
       tft.setCursor(0, 150);   
       tft.print(myPosition2);   
      }
    else if (myPosition2 >= 10)   
      {
       tft.print("   ");
       tft.setCursor(0, 150);   
       tft.print(myPosition2);   
      }
    }   
  }






  //  Read the "output" value of the currently selected preset and update the encoder logic which should in turn update the number on LCD
 
  if (digitalRead(4) == HIGH)   
  {}
  else
  {
        
    //util_getCtrlValue("output");
    //??????
    //newPosition == 
    
  }
 
 }
    
    delay(30);
}
 
Looking at util.c it seems that util_getCtrlValue and util_listCtrlValues print to the serial monitor.
Can anyone suggest an approach to store the return of util_listCtrlValues so I can use them in the code? it outputs a label then a value and repeats, input1_gain 72 input2_gain 183 etc.


void util_listCtrlValues ()
{
const int tCtrls = ctrl_c4_total();

for (int i = 0; i < tCtrls; i++){
ctrl_t *ctrl = ctrl_c4_get(i);
if (ctrl->getIdx == -1) continue;

as_getControlValue(NULL, ctrl->getIdx, &ctrl->u.val8);

if (ctrl->width < 8){
ctrl->u.val8 >>= ctrl->bitPosition;
ctrl->u.val8 &= ctrlBitmasks[ctrl->width];
}

printf("%-22s %-3i\r\n", ctrl->label, ctrl->u.val8);
}
}

void util_getCtrlValue (const char *name)
{
ctrl_t *ctrl = ctrl_c4_find(name);
if (ctrl){
if (ctrl->getIdx == -1){
printf("not implemented\r\n");
return;
}
uint8_t ctrlIdx = ctrl->getIdx&0xFF;
uint8_t value8 = 0;

// use ctrlIdx+n for each byte of 16/32bit values
as_getControlValue(NULL, ctrlIdx, &value8);

if (ctrl->width < 8){
value8 >>= ctrl->bitPosition;
value8 &= ctrlBitmasks[ctrl->width];
}

//printf("%s: %i\n", ctrl->label, value8);
printf("%i\r\n", value8);
}else{
printf("Control '%s' not recognised\r\n", name);
}
}
 
Last edited:
Back
Top