Arduino Analog Keypad Mozzi Synth

Back when I was messing about with LDRs I made the comment that there are many different kinds of sensors you could use to replace potentiometers in a synth.  Having now had a play with analog keypads, I thought they’d be a fun thing to try.  The conclusion – it works, but I’m still deciding if it is in anyway useful!

Warning! I strongly recommend using old or second hand equipment for your experiments.  I am not responsible for any damage to expensive instruments!

These are the key Arduino tutorials for the main concepts used in this project:

If you are new to Arduino, see the Getting Started pages.

Parts list

The Circuit

AnalogKeypadSynthController_bb

This circuit has up to six analog keypads connected to the Arduino’s analog input ports as previously described. If you are using less  you need to decide if you want to use potentiometers as the remaining inputs or to fix their values in software.

This project produces a PWM audio output on pin 9 which (if you’re brave) you could connect to a speaker or some kind of amplifier circuit, but I’d recommend at least some rudimentary filtering as described in my Arduino PWM Output Filter Circuit.

IMG_5269

The Code

I took the code from my Arduino Multi-pot Mozzi FM Synthesis engine but wanted to fix the envelope generation and add two other controls, so I opted to add in a low-pass filter (a little like that used on the Helios One synth) and to build back in analog control of the frequency rather than drive it over MIDI.

Consequently you can see some additional conditional compilation options in the code for extra pot control:

//#define AD_A_PIN 4 // ADSR Attack
//#define AD_D_PIN 5 // ADSR Delay
#define LPFF_PIN 4 // LPF Cutoff Frequency
#define FREQ_PIN 5 // Pot-controlled base frequency (i.e. no MIDI, no envelope)

In my case I’ve disabled the envelope controls and re-assigned the analog pins to these new controls then added the appropriate code to support default values and read the pots as required.

There was one “gotcha” though.  The original code basically did the following:

potnumber++
IF potnumber > 6 THEN set potnumber to 0
WHEN potnumber=0: handle wave selection
WHEN potnumber=1: handle FM intensity
...and so on up to potnumber=5...
DEFAULT set potnumber to 0

Rather than having a specific test for a specific number of pots at the top, I thought I’d be clever and remove the test and let the default case reset potnumber back to 0, so after adding in my two new cases I ended up with this:

potnumber++
WHEN potnumber=0: handle wave selection
WHEN potnumber=1: handle FM intensity
...and so on up to potnumber=7...
DEFAULT set potnumber to 0

But then spent a good hour or so trying to work out why I’d broken the wave selection… Basically, after the default sets potnumber to 0, the first line in the processing increments it straight to 1 without even being able to actually run the “potnumber=0” case!  Sigh.

This is the updated code:

potnumber++
WHEN potnumber=1: handle wave selection
WHEN potnumber=2: handle FM intensity
...and so on up to potnumber=8...
DEFAULT set potnumber to 0

Low-pass Filter

To add the low-pass filter, I’ve created a function to update the filter that needs to be called from the control loop.  This is based on the “low pass filter” example from the Mozzi library.

void setLpf() {
  if (potLPFF < 2) {
    lpfEnable = false;
  } else {
    lpfEnable = true;
  }
  byte cutoff_freq = 100 + potLPFF/2;
  lpf.setCutoffFreqAndResonance(cutoff_freq, lpfResonance);
}

The original example uses a modulator for the cut-off frequency that changes randomly roughly every half second, but I wanted it fixed and controlled by the pot instead, so took that out  (well, it is still in the code, but unused).  Note that this code assumes the pot will be suitably scaled when read to be a 0-255 value which is achieved with the following

 potLPFF = mozziAnalogRead(LPFF_PIN) >> 3; // value is 0-255

What I did want though was the option to completely disable the filter, so I decided if the pot value was near zero, I’d remove the filter from the processing in updateAudio.

Frequency Control

The other key functionality is the frequency pot.  I didn’t want to take out MIDI, but just have it that if the frequency pot is non-zero then that will be used instead rather than a MIDI-derived frequency and an envelope.  The simplest way to do that seemed to be check the pot in the existing “setFreqs” function.

void setFreqs(){
  if (potFREQ != 0) {
    freqEnable = true;
    carrier_freq = 20+potFREQ*2;
  } else {
    freqEnable = false;
  }

  int mod_freq = carrier_freq * mod_ratio;
  aCarrier.setFreq(carrier_freq);
  aModulator.setFreq(mod_freq);
}

And then once again some conditional statements in updateAudio.

Audio Processing Chain

updateAudio is getting a little messy, but it does allow for pretty quick checking to see quite what kind of updating we are doing – with envelopes or without, with the filter or without.  The resulting function is as follows.

AudioOutput_t updateAudio(){
  long modulation = aSmoothIntensity.next(fm_intensity) * aModulator.next();
  if (lpfEnable) {
    char asig = lpf.next(aCarrier.phMod(modulation));
    if (freqEnable) {
      return MonoOutput::from8Bit(asig);
    } else {
      return MonoOutput::from16Bit(envelope.next() * asig);
  }
  } else {
    if (freqEnable) {
      return MonoOutput::from8Bit(aCarrier.phMod(modulation));
    } else {
      return MonoOutput::from16Bit(envelope.next() * aCarrier.phMod(modulation)); 
    }
  }
}

If you compare with this previous code you’ll see I’ve also updated it to use the newer Mozzi approach for scaling return values to best fit the architecture being used (in this case the Arduino Uno).  In the previous code when applying the envelope, the result would have needed shifting right by 8 (i.e. >>8) to perform a “divide by 256” to cope with the fact we are multiplying two 8-bit values together (envelope and sample).  Now, I can just use the “from16Bit” function to take care of it for me.

This all results in the following processing chain:

  • A control rate modulator for the FM intensity (kSmoothIntensity)
  • An audio rate modulator signal (aModulator)
  • An audio rate carrier signal (aCarrier).
  • A modulation value is obtained from combining the smoothed FM intensity with the latest modulator signal value.
  • The modulated sample is obtained from using the modulation value in the pdMod() function of the carrier signal.
  • Then there is one of four possible outputs from this point onwards:
    • no filter, no envelope – returns the modulated sample “as is”.
    • no filter, but envelope – returns the modulated sample with additional modulation from the envelope.
    • filter, no envelope – passes the modulated sample into the LPF and returns the result.
    • filter, plus envelope – passes the modulated sample into the LPF and adds additional modulation to create the envelope.

Analog Keypad Scaling

This all worked fine with six pots, which I used for initial testing, but upon replacing the pots with analog keypads I wasn’t really benefiting from the full range of options for each input.  This is because the switches generate values in the range 480 to 1023, so there is a large jump from 0 (no switches pressed) to 480 (first switch pressed) in the readings, missing out a good chunk of resolution.

The solution was to include a “keypad scaling” function to be used instead of the mozziAnalogRead function to scale all potentiometers before any further processing occurs.

int keypadAnalogRead (int pin) {
  int val = mozziAnalogRead(pin);
  return map (constrain(val, 460, 1023), 460, 1023, 0, 1023);
}

One thing I hadn’t appreciated until now is that the Arduino map() function doesn’t “cut off” at the minimum and maximum values. As it says on the tutorial page “Does not constrain values to within the range, because out-of-range values are sometimes intended and useful.”  To limit the range of value you have to use the constrain() function too which ensures the value will always be within the specified bounds.

This is a straight drop-in replacement for the mozziAnalogRead function, which itself was meant to be a drop-in replacement for the Arduino’s own analogRead function.

I’ve opted to add this in as another compilation option, as follows:

#ifdef ALGKEYPAD
#define ALGREAD keypadAnalogRead
#else
#define ALGREAD mozziAnalogRead
#endif

Then all calls in the control loop that read potentiometers are then re-written to use ALGREAD, with everything else kept the same, as follows:

// potWAVT = mozziAnalogRead(WAVT_PIN) >> 8; // value is 0-3
potWAVT = ALGREAD(WAVT_PIN) >> 8; // value is 0-3

Find it on GitHub here.

Closing Thoughts

I’m still deciding if this is an interesting way of controlling a synth. I think it has potential (as it were) but it isn’t there yet. I suspect to be really “playable” in any real sense it would need tailoring to the arrangement of the buttons themselves, which rather defeats the object of “drop in replacement for a potentiometer”.

Kevin

IMG_5268

Leave a comment