Skip to content

Instantly share code, notes, and snippets.

@tfry-git
Last active June 14, 2024 18:16
Show Gist options
  • Save tfry-git/58c27f8b23d11f0a49b5e4671f4fa531 to your computer and use it in GitHub Desktop.
Save tfry-git/58c27f8b23d11f0a49b5e4671f4fa531 to your computer and use it in GitHub Desktop.
/* (Not so) simple synth based on Mozzi library and a bunch of pots.
*
* This code is derived from 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.
*
* Circuit: Audio output on digital pin 9 (on a Uno or similar), or
* check the README or http://sensorium.github.com/Mozzi/
*
*
* 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>
// 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)
#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!
#define NOTECOUNT 2
// Rate (Hz) of calling updateControl(), powers of 2 please.
#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.
AttackPot,
ReleasePot,
/* // Additive Synthesis
WaveForm2Pot,
Oscil2OctavePot,
Oscil2DetunePot,
Oscil2MagnitudePot, */
// 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(){
MIDI.begin();
MIDI.setHandleNoteOn(MyHandleNoteOn);
MIDI.setHandleNoteOff(MyHandleNoteOff);
for (byte i = 0; i < NOTECOUNT; ++i) {
notes[i].env.setADLevels(200,100);
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.
pots[WaveFormPot] = mozziAnalogRead(A0);
//pots[OctavePot] = mozziAnalogRead(A1); // Skipped
pots[AttackPot] = mozziAnalogRead(A2);
pots[ReleasePot] = mozziAnalogRead(A3);
/* // Additive Synthesis
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
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
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.
// 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) {
// 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.
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
// 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.
}
}
}
@tfry-git
Copy link
Author

tfry-git commented Oct 9, 2019

Hi! This is somewhat hard to diagnose over the wire. You removed / disabled line #57, right? And now your pots don't seem to have an effect. How are they connected? Did you measure the voltage levels on the analog pins to make sure the hardware is setup up, correctly?

You could try inserting some Serial.print()-lines to readPots() (this will only work with FAKE_MIDI defines), to make sure the values are read, correctly.

@SimonePie
Copy link

Thanks for the suggestion tonight I try to do as you said. I will let you know. Thanks again

@SimonePie
Copy link

You were right a positive thread was defective.
I also tried the midi keyboard and it works very well. Again congratulations for the project. Have you published other interesting projects?

@tfry-git
Copy link
Author

Thanks for the positive feedback! I also have a few repositories at https://github.com/tfry-git . One is a beefed up Syntheziser project based on a (cheap) STM32 board. It is not quite finished, and does not come with a lot of documentation, though.

@NessunoNiemand
Copy link

NessunoNiemand commented Feb 2, 2020

hi i like so mutch the sounds in this code.is there any solution to use this code as a drone synth with no midi input?sorry but my experience with the code is not so good.thanks

@tfry-git
Copy link
Author

tfry-git commented Feb 3, 2020

@NessunoNiemand: Sure, you can, but you'll have to dig into programming at least some. In very rough steps, you could create an array of notes to play (each with pitch, velocity(=loudness), and duration). Then you'd feed those into MyHandleNoteOn() one at a time. Arguably the best place to insert this is where the FAKE_MIDI section is right now. All you'd have to do is replace the random value with pre-programmed ones.

@NessunoNiemand
Copy link

@tfry-git: thank you so much. I will study more and then i will have a try!

@sergiomirandabonilla
Copy link

Thank you for sharing this project. I'd ask you: How would you restore the octave pot? Original e-licktronick sketch simply multiplies frequency times octave pot mapped value, but how can this be included in your code? I'm a very slow code apprentice, but i think this can be done by redesignating analog input and retrieved value:

void updateControl()
if(value_pot1<= 256) oct =4;
else if (value_pot1<=512 && value_pot1>256) oct=2;
else if (value_pot1<=768 && value_pot1>512) oct=1;
else if (value_pot1>768) oct=0.5;

void MyHandleNoteOn(byte channel, byte pitch, byte velocity) {
if (velocity > 0) {
f = mtof(pitch);
f=f/oct;
aSaw.setFreq(f);
aSin.setFreq(f);
aTri.setFreq(f);
aSqu.setFreq(f);
env.noteOn();
}
else {
env.noteOff();
}
}

Apparently, original e-licktronick sketch relied on an outdated version of the library, and I'm having a real headache trying to make this work. Any help would be greatly appreciated.

@tfry-git
Copy link
Author

tfry-git commented Sep 8, 2020

How would you restore the octave pot?

The code already has some - commented out - uses of OctavePot. Re-enable those. Then, change line 260 to

int f = mtof(pitch + 12*(pots[OctavePot] >> 8));

(pitch is the midi note value. Changing this by twelve corresponds to one octave. f is the resulting frequency.)

@sergiomirandabonilla
Copy link

How would you restore the octave pot?

The code already has some - commented out - uses of OctavePot. Re-enable those. Then, change line 260 to

int f = mtof(pitch + 12*(pots[OctavePot] >> 8));

(pitch is the midi note value. Changing this by twelve corresponds to one octave. f is the resulting frequency.)

Thank you for your kind answer, @tfry-git. I turned off fake MIDI and fake pots, uncommented lines 73 and 158 and swapped line 260. But I'm getting this error at compiling. I'm trying to format this to code, but the spaces get lost, so I screen captured it.
Error

Do I have to modify another line in your sketch, or to edit the library (wich I totally don't want or know how to do)?

Thank you in advance.

@tfry-git
Copy link
Author

tfry-git commented Sep 9, 2020

int f = mtof((uint8_t) (pitch + 12*(pots[OctavePot] >> 8)));

should do the trick, then.

@sergiomirandabonilla
Copy link

int f = mtof((uint8_t) (pitch + 12*(pots[OctavePot] >> 8)));

should do the trick, then.

It's alive! Thank you very much, @tfry-git, it works like a charm with MIDI input and all 8 pots working.

I found out that final mix volume may be backed a bit by shifting 6 to 7 at line 240 for a smoother output. I also reversed the LED behavior to start with light on, and set off when MIDI note arrives (that wasn't too hard). I'm getting more of the language syntax. Thank you.

Despite the fact I'm a noob at C++ math and numbers, I'm taking the risk to tweak your code a bit more. I may activate the second oscilator with fixed parameters (I won't add any muxs) and see how it turns out.

The real challenge (and basic need) for me is to make your Synth listen to MIDI Control Change messages. I already changed preset env sustain value to 60000 in order to get max 6 minutes of sustained note until Note Off arrives. But I want it to sustain the note when MIDI CC 64 (sust pedal) arrives with 127 value, and enter env release phase when CC64 restores 0 value. I'm a guitar player, so that will help me to get backing drone harmony. I'm not sure if this has to be dealt with HandleControlChange funtion, and code some if, case or for statements inside HandleNoteOff function. I you don't mind, since you have been so kind, I may ask later. Cheers from México, and thanks again.

@schutzero
Copy link

schutzero commented Jan 17, 2021

Hello @tfry-git , great work!

I tried the code some months ago and it was working great but with the lastest adition of notes[i].setTable(WAVE_TABLES[0]); the code is no longer compiling, what is the purpose of this new adition? thanks again for this great project.

@tfry-git
Copy link
Author

@schutzero : Thanks for pointing out my mistake. I was toying with my sketch on ESP32 and found it it was crashing due to the oscillator not being initialized. Then, I messed up while re-applying my fix, here. The line should be notes[i].oscil.setTable(WAVE_TABLES[0]);, instead.

@schutzero
Copy link

NICE! Is this now working on ESP32? which should be the output audio pin now? there is no documentation regarding the ESP32 on the mozzi website.

Thanks in advance!

@tfry-git
Copy link
Author

tfry-git commented Feb 1, 2021

The Mozzi website is a little out of date (and ESP32 support is still fresh). Use git master, and refer to README.md for details. Several output modes can be configured on the ESP32, the default using the internal DAC (which is limited to 8bit resolution), at pins 25 and 26.

@schutzero
Copy link

@tfry-git, Thanks!

@schutzero
Copy link

Hello!, I've been playing with this sketch using multiplexers, any clues on how to enable the octave pot for the second oscillator (in the additive synthesis part)?

@donluca
Copy link

donluca commented Jan 24, 2022

Hi @tfry-git , I've forked this gist and I'm working on it myself to expand its capabilities.
I've read that you were trying to make it work on an ESP32 which is the same board I have and on Mozzi's github page I've read that when using the PDM_VIA_I2S you have to scale the output samples to full 16 bit range in updateAudio(), but found no example of how to do this. Does your sketch already take care of this? Thanks!

@tfry-git
Copy link
Author

Well, the sketch as is outputs around 9 bits in updateAudio(), and in order to achieve that, it shifts to the right by six bits (>> 6). Replace that with (<< 1) and you should end up at 16 bits, instead.

Actually, as a more modern approach, you'd use
AudioOutput::fromNBits(15, ret);
instead of hard-coded shifting.

@donluca
Copy link

donluca commented Jan 24, 2022

Thank you so much! That makes perfect sense, tomorrow I'll try giving it a go and see if I can hear a difference.

Unfortunately the output from PDM_VIA_I2S is pretty noisy, even by increasing the PDM_RESOLUTION, maybe I'll have to use an external DAC to get a cleaner output.

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