Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save donluca/31d22b18bf94c9eea37693e86465faf1 to your computer and use it in GitHub Desktop.
Save donluca/31d22b18bf94c9eea37693e86465faf1 to your computer and use it in GitHub Desktop.
/* (A definitely not) simple synth based on Mozzi library and a lots of pots.
*
* This code is derived from tfry-git "Aruidno 7 potentiometer Synth"
* (https://gist.github.com/tfry-git/58c27f8b23d11f0a49b5e4671f4fa531)
* which in turn is based on the public domain example of an 8 potentiometer
* synth from e-licktronic (https://www.youtube.com/watch?v=wH-xWqpa9P8).
*
* Severely edited for clarity and configurability, adjusted to run with modern
* versions of Mozzi, extended for polyphony by Thomas Friedrichsmeier. Also,
* this sketch will auto-generate fake MIDI events and random parameters, so
* you can start listening without connecting anything other than your
* headphones or amplifier. (Remove the FAKE_POTS and FAKE_MIDI defines, once
* you connect to real hardware).
*
* Note that this sketch does not use any port multiplexing, thus to use all
* seven pots, you need a board with seven or more analog inputs. The Arduino
* Nano seems like a perfect match, but some Pro Mini clones also make A6 and A7
* available.
* In particular, this version uses 17 potentiometers which should work alright
* with an ESP32 board which has 18 Analog pins, 16 of which are available (and
* even then, some of those with some caveats).
*
* Circuit: Audio output on digital pin 9 (on a Uno or similar), or
* check the README or http://sensorium.github.com/Mozzi/
* On the ESP32 this is defaulted to pin 33, but this is an analog input pin
* and we don't want to give it up, so we'll have to remap it to another pin
* in Mozzi's AudioConfigESP32.h
*
*
* Copyright (c) 2017 Thomas Friedrichsmeier
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <MIDI.h>
#include <MozziGuts.h>
#include <Oscil.h>
#include <mozzi_midi.h>
#include <mozzi_rand.h>
#include <ADSR.h>
#include <LowPassFilter.h>
#include <WiFi.h>
// The waveforms to use. Note that the wavetables should all be the same size (see TABLE_SIZE define, below)
// Low table sizes (512, here), help to keep the sketch size small, but are not as pure (which may not even be a bad thing)
// Working with an ESP32 we can go overboard and put many more tables and with bigger table sizes
#include <tables/sin512_int8.h>
#include <tables/saw_analogue512_int8.h>
#include <tables/triangle512_int8.h>
#include <tables/square_analogue512_int8.h>
#define NUM_TABLES 4
const int8_t *WAVE_TABLES[NUM_TABLES] = {SQUARE_ANALOGUE512_DATA, SIN512_DATA, SAW_ANALOGUE512_DATA, TRIANGLE512_DATA};
#define TABLE_SIZE 512
// Fake Pots for easy testing w/o hardware. Disable the line below, once you have real pots connected
#define FAKE_POTS
// Fake MIDI input for easy testing. Disable the line below, if you have real MIDI in
#define FAKE_MIDI_IN
// number of polyphonic notes to handle at most. Increasing this carries the risk of overloading the processor
// For an ATMEGA328 at 16MHz, 2 will be the limit, or even just one, if you increase the complexity of the synthesis.
// But on a 32bit CPU, you can up this, considerably!
// Testing is needed but on an ESP32 I don't think polyphony will be an issue, we can raise this to 6 (two triads chord)
#define NOTECOUNT 2
// Rate (Hz) of calling updateControl(), powers of 2 please.
// Testing needed, but I think we can improve this as well, 128 should be fine, I'm not sure if anything more would be overkill
#define CONTROL_RATE 64
// The point of this enum is to provide readable names for the various inputs.
// For the mapping of analog pins to parameter, see the function readPots(), further below.
enum Potentiometers {
WaveFormPot,
// OctavePot, // Octave of primary oscil. The E-licktronics synth has it, but I did not see the point, and decided to skip this.
// We'll forego this as well since we'll be using either a sequencer with this function builtin or a keyboard
AttackPot,
ReleasePot,
// We really want two more pots here for a complete ADSR control, Decay Level is the same as Sustain Level
DecayPot,
SustainPot,
// Additive Synthesis
// We're going to enable this and add ADSR control as well
WaveForm2Pot,
Oscil2OctavePot,
Oscil2DetunePot,
Oscil2MagnitudePot,
Oscil2AttackPot,
Oscil2ReleasePot,
Oscil2DecayPot,
Oscil2SustainPot,
// Modulated LPF
LPFCutoffPot,
LPFResonancePot,
LFOSpeedPot,
LFOWaveFormPot,
POTENTIOMETER_COUNT
};
uint16_t pots[POTENTIOMETER_COUNT];
class Note {
public:
byte note; // MIDI note value
int8_t velocity;
Oscil<TABLE_SIZE, AUDIO_RATE> oscil;
ADSR<CONTROL_RATE, CONTROL_RATE> env;
int8_t current_vol; // (envelope * MIDI velocity)
uint8_t osc2_mag; // volume of Oscil2 relative to Oscil1, given from 0 (only Oscil1) to 255 (only Oscil2)
bool isPlaying () { return env.playing (); };
// Modulated LPF as in the E-Licktronic example.
// NOTE: Here, I'm using separate LFOs for each note, but there's also a point to be made for keeping all LFOs in sync (i.e. a single global lfo).
Oscil<512, CONTROL_RATE> lfo; // set up to osciallate the LPF's cutoff
LowPassFilter lpf;
uint8_t lpf_cutoff_base;
bool lfo_active;
Oscil<TABLE_SIZE, AUDIO_RATE> oscil2;
};
Note notes[NOTECOUNT];
#if defined(FAKE_MIDI_IN)
#include <EventDelay.h>
EventDelay noteDelay;
#endif
MIDI_CREATE_INSTANCE(HardwareSerial, Serial, MIDI);
void setup(){
WiFi.mode(WIFI_OFF); // Turn off WiFi, otherwise ADC2 pins are not available
btStop(); // Turn off Bluetooth
MIDI.begin();
MIDI.setHandleNoteOn(MyHandleNoteOn);
MIDI.setHandleNoteOff(MyHandleNoteOff);
for (byte i = 0; i < NOTECOUNT; ++i) {
// At some point we'll have to modify setADLevels to just setAttackLevel(200) and use setDecayLevel with an analog pot later on.
notes[i].env.setADLevels(200,100);
// Same here: setDecayTime will be set by an analog pot
notes[i].env.setDecayTime(100);
notes[i].env.setSustainTime(1000);
notes[i].oscil.setTable(WAVE_TABLES[0]);
notes[i].note = 0;
}
#if FAKE_MIDI
noteDelay.set (1000);
#endif
randSeed();
startMozzi(CONTROL_RATE); // set a control rate of 64 (powers of 2 please)
}
const int8_t *potValueToWaveTable (unsigned int value) {
for (uint8_t i = 0; i < NUM_TABLES-1; ++i) {
if (value <= (1024 / NUM_TABLES)) return (WAVE_TABLES[i]);
value -= (1024 / NUM_TABLES);
}
return WAVE_TABLES[NUM_TABLES-1];
}
void readPots() {
#if defined(FAKE_POTS)
// Fake potentiometers: Fill with random values
for (int i = 0; i < POTENTIOMETER_COUNT; ++i) {
pots[i] = rand (1024); // Mozzis rand() function is faster than Arduinos random(), and good enough for us.
}
pots[LPFCutoffPot] = pots[LPFCutoffPot] >> 1 & (1023); // Lower cutoffs reserved for actual user interaction, as they can turn off sounds, completely.
pots[LPFResonancePot] = pots[LPFResonancePot] << 1; // Similarly, keep resonance to a safe limit for random parameters
#else
// Real potentiometers. Adjust the pot mapping to your liking.
// We need to add a bunch of these later on and figure out multiplexing because we don't have enough analog inputs
pots[WaveFormPot] = mozziAnalogRead(A0);
//pots[OctavePot] = mozziAnalogRead(A1); // Skipped
pots[AttackPot] = mozziAnalogRead(A2);
pots[ReleasePot] = mozziAnalogRead(A3);
/* // Additive Synthesis – Need to add all the others and map them as well
WaveForm2Pot,
Oscil2OctavePot,
Oscil2DetunePot,
Oscil2MagnitudePot, */
// Modulated LPF
pots[LPFCutoffPot] = mozziAnalogRead(A4);
pots[LPFResonancePot] = mozziAnalogRead(A5);
pots[LFOSpeedPot] = mozziAnalogRead(A6);
pots[LFOWaveFormPot] = mozziAnalogRead(A7);
#endif
}
// Update parameters of the given notes. Usually either called with a single note, or all notes at once.
void updateNotes (Note *startnote, uint8_t num_notes) {
unsigned int attack = map(pots[AttackPot],0,1024,20,2000);
unsigned int releas = map(pots[ReleasePot],0,1024,40,3000);
// LPF
float speed_lfo = map(pots[LFOSpeedPot],0,1024,.1,10);
uint8_t cutoff=map(pots[LPFCutoffPot],0,1024,20,255);
uint8_t resonance;
if (potValueToWaveTable(pots[WaveFormPot])==WAVE_TABLES[1]) resonance=map(pots[LPFResonancePot],0,1024,0,120); // Special casing for sine waves: Use somewhat lower resonance, here
else resonance=map(pots[LPFResonancePot],0,1024,0,170);
for (uint8_t i = 0; i < num_notes; ++i) {
startnote[i].env.setAttackTime(attack); //Set attack time
startnote[i].env.setReleaseTime(releas);//Set release time
// Ok, so this is the place where we're going to set the decay level (sustain) and decay time
startnote[i].oscil.setTable (potValueToWaveTable(pots[WaveFormPot]));
// LPF
startnote[i].lfo_active = pots[LPFCutoffPot] >= 5; // fully disable LFO on very low frequency
if (startnote[i].lfo_active) {
startnote[i].lfo.setTable (potValueToWaveTable(pots[LFOWaveFormPot]));
startnote[i].lfo.setFreq (speed_lfo);
}
startnote[i].lpf.setResonance(resonance);
startnote[i].lpf_cutoff_base = cutoff;
/* // Wave mixing
// We'll have to enable this at some point and figure out how to add ADSR to it
notes[i].oscil2.setTable (potValueToWaveTable(pots[WaveForm2Pot]));
notes[i].oscil2.setFreq (mtof (notes[i].note + 28 - (pots[Oscil2OctavePot] >> 8) * 12 - pots[Oscil2DetunePot] >> 5));
notes[i].osc2_mag = pots[Oscil2MagnitudePot] >> 2; */
}
}
void updateControl(){
MIDI.read();
#if defined(FAKE_MIDI_IN)
if (noteDelay.ready ()) {
MyHandleNoteOn (1, rand (20) + 77, 100);
noteDelay.start (1000);
}
#endif
#if !defined(FAKE_POTS) // Fake (random!) pots should not update on every control cycle! They fluctuate too much.
readPots();
#endif
// If you enable the line below, here (and disable the corresponding line in MyHandleNoteOn(), notes _already playing_ will be affected by pot settings.
// Of course, updating more often means more more CPU load. You may have to reduce the NOTECOUNT.
// ...but we're on an ESP32 so we don't really care.
updateNotes (notes, NOTECOUNT);
for (byte i = 0; i < NOTECOUNT; ++i) {
notes[i].env.update ();
notes[i].current_vol = notes[i].env.next () * notes[i].velocity >> 8;
if (notes[i].lfo_active) notes[i].lpf.setCutoffFreq((notes[i].lpf_cutoff_base*(128+notes[i].lfo.next()))>>8);
else (notes[i].lpf.setCutoffFreq(notes[i].lpf_cutoff_base));
}
}
int updateAudio(){
int ret = 0;
for (byte i = 0; i < NOTECOUNT; ++i) {
// We need to enable this and disable the following line
// We also need to shift bits to have full 16-bit resolution on ESP32 by replacing ">> 6" with "<< 1"
// A more modern approach would be using AudioOutput::fromNBits(15,ret); but right now we're still testing stuff out so we'll leave it be
// ret += ((long) notes[i].current_vol * ((notes[i].oscil2.next() * notes[i].osc2_mag + notes[i].oscil.next() * (256u - notes[i].osc2_mag)))) >> 14; // Wave mixing
ret += ((int) notes[i].current_vol * notes[i].lpf.next(notes[i].oscil.next())) >> 6; // LPF
}
return ret;
}
void loop(){
audioHook(); // required here
}
void MyHandleNoteOn(byte channel, byte pitch, byte velocity) {
if (velocity > 0) {
for (byte i = 0; i < NOTECOUNT; ++i) {
if (!notes[i].isPlaying ()) {
#if defined(FAKE_POTS)
readPots();
#endif
// Initialize current note with current parameters. Depending on your taste and usecase, you may want to disable this, and enable the corresponding line
// inside updateControl(), instead.
// That's exactly what we're going to do.
// updateNotes(&notes[i], 1);
int f = mtof(pitch);
notes[i].note = pitch;
notes[i].oscil.setPhase (0); // Make sure oscil1 and oscil2 start in sync
notes[i].oscil.setFreq (f);
notes[i].env.noteOn();
notes[i].velocity = velocity;
// LPF
notes[i].lfo.setPhase (TABLE_SIZE / 4); // 90 degree into table; since the LFO is oscillating _slow_, we cannot afford a random starting point */
// Wave mixing
// Again, we'll have to enable this
// notes[i].oscil2.setPhase (0);
break;
}
}
} else {
MyHandleNoteOff (channel, pitch, velocity);
}
}
void MyHandleNoteOff(byte channel, byte pitch, byte velocity) {
for (byte i = 0; i < NOTECOUNT; ++i) {
if (notes[i].note == pitch) {
if (!notes[i].isPlaying ()) continue;
notes[i].env.noteOff ();
notes[i].note = 0;
//break; Continue the search. We might actually have two instances of the same note playing/decaying at the same time.
}
}
}
@donluca
Copy link
Author

donluca commented Jan 9, 2022

Note to self:

  • Enable Oscil2 and Wave Mixing
  • Read notes[i].env.setDecayTime(100) from an analog pot
  • Read Sustain Level with notes[i].env.setADLevels(200,100) – (second parameter) from an analog pot (Decay Level = Sustain Level)
  • Add ADSR to Oscil2
  • Total pots needed: 17
  1. Oscil1WaveForm
  2. Oscil1AttackTime
  3. Oscil1DecayTime
  4. Oscil1SustainLevel (DecayLevel)
  5. Oscil1ReleaseTime
  6. Oscil2WaveForm
  7. Oscil2AttackTime
  8. Oscil2DecayTime
  9. Oscil2SustainLevel (DecayLevel)
  10. Oscil2ReleaseTime
  11. Oscil2Octave
  12. Oscil2Detune
  13. Oscil2Magnitude
  14. LPFCutOff
  15. LPFResonance
  16. LFOSpeed
  17. LFOWaveForm

ESP32 has 18 Analog inputs but 3 of them (GPIO 0, 2 and 15) might be problematic as per https://www.esp32.com/viewtopic.php?t=5970

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment