Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save FluxGarage/327f29893374b50187ba0cc9ae98bbff to your computer and use it in GitHub Desktop.
Save FluxGarage/327f29893374b50187ba0cc9ae98bbff 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].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.
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment