Tried to add pre-run for all the switches and pots of Mega 1, so main loop can run with properly initialized values. Failed to do so due to some obscure bug, maybe linked to arduino map() function, and maybe to uC fault.

Moved MIDI offset from Mega 1 to Teensy, so it can be applied only for internal midi, and note for usb.
Added an intermediate function for note on and note off, to add offset from internal midi.
Modified keyboard hidden settings.
Modified mod wheel handling (almost ok) and pitch bend as well (no that good).
Added infos to readme, and pictures.
pull/1/head
Pierre-Loup Martin 5 years ago
parent f32837876b
commit d488092063
  1. 90
      README.MD
  2. 2
      minimoog_mega_1/defs.h
  3. 97
      minimoog_mega_1/minimoog_mega_1.ino
  4. 13
      minimoog_mega_2/minimoog_mega_2.ino
  5. 11
      minimoog_teensy/audio_setup.h
  6. 104
      minimoog_teensy/minimoog_teensy.ino
  7. BIN
      misc/pictures/1.jpg
  8. BIN
      misc/pictures/2.jpg
  9. BIN
      misc/pictures/3.jpg
  10. BIN
      misc/pictures/4.jpg

@ -1,4 +1,6 @@
# TeensyMoog
![teensymoog 1](https://github.com/troisiemetype/teensymoog/misc/picures/1.jpg)![teensymoog 2](https://github.com/troisiemetype/teensymoog/misc/picures/2.jpg)
## What is it ?
_Teensymoog_ is a synth, inspired by the legendary minimoog.
@ -6,3 +8,91 @@ It aims at looking and functionning as close as possible of the orinal, but usin
It uses three boards, one Teensy 4.0 that handles the heavy computing (sound generation), and two Arduino Mega (chinese copies in a small form factor), that are used as "port expander", which read keys, switches and potentiometers and send their value on change to the Teensy.
## How it's made :
It's based on a Teensy 4.0 running the incredible audio library by _Paul Stoffregen_ of [PJRC.com](https://www.pjrc.com/teensy/td_libs_Audio.html).
The whole interface (`minimoog_teensy/audio_setup.h`) has been developped with the associate [visual online audio system design tool](https://www.pjrc.com/teensy/gui)
Two Arduino Mega pro are used to poll all the switches and potentiometer and sending any update to the teesny. The communication between the boards uses MIDI messages, mainly control change (or continuous control) messages. I've tried to stick as close as possible to the MIDI specification, and using the existing and available control numbers. There are cases where it's not the case (_e.g._ filter cutoff frequency uses a two bytes control change to increase resolution).
A Pimoroni's phatDAC is used to output high quality audio from the i2s stream generated by the audio library. A USB B port is mounted on the rear panel so the synth can be connected to anything, as a USB midi device or USB audio device (not available in Arduino IDE for Teensy 4.0 yet, but I know it will come !). There is also provision for (not implemented yet !) MIDI in and out (DIN-5 pins, but maybe 3.5 jack would be a better idea) sustain and expression.
A smartphone charger has been repurpose as the main adaptor.
The enclosure has been made out of would and acrylic.
The base plate is made of plywood. The mains parts are made of walnut, from a table I found in the street two years ago. For a long time I wanted to repair it, but I didn't like its feet, so this is a good opportunity to make room in there ! The wood has been treated with tung oil, wich gives a soft and smooth finish that I really love.
The main and lateral control panels, control panel bottom and rear enclosure are made of 2mm acrylic (PMMA). It has been cut and engrave with a CO2 laser cutter. The engravures are filled with white paint. SVG files for the panels are included in the repository.
There was no sketch made for the walnut enclosure, but I should have done some : it's quite thin (around 7cm tall), and it gave me headaches to integrate some components in it. There is plenty of room, but I had to change things several time to accomodate the keyboard height, the front panel with sontrols and the rear panel with plugs...
## Function implemented
As said above, the goal is to have something looking as close as possible to the original Minimoog.
The keyboard is smaller, 2 1/2 cotaves instead of 3 1/2 (or 30 keys instead of 44), not by choice, but because the _Bontempi_ electric organ it cames from.
### Global description
It has three oscillators, a mixer section with noise and feedback (post-filter output is re-fed to the mixer), a filter, two envelopes generator for filter and notes, a LFO, a pitchbend wheel and a modulation wheel.
### Oscillators
Oscillators have each six waveforms to choose from, six requency range (or octave transposition) and osc. 2 & 3 can be detuned by +- 1 octave regarding the base note. Osc.3 can be disconnected from the keyboard control, and used as a drone.
### Noise
There is one noise source, with pink and white noise.
### Feedback
The original minimoog has a external input mixer entry, so anything (guitar, another synth, etc) could be processed through the mixer. It was often used as a feedback path by plugin the headphone jack back into this input. The minimoog reissue has hard-wired this by connecting the output to this external input internally when nothing is connected to the jack.
Maybe external input will be implemented once, but I wanted this feedback.
### Mixer
the mixer is copy-paste on the original minimoog : a potentiometer for each of the five channels, and a switch for rapid on / off.
### Filter
The filter is (I believe) close from the minimoog one. Cutoff frequency and emphasis (resonance) are available. Their is an associated envelope generator that modulates the cutoff frequency. Their is also an addition compared to the minimoog : there is a knob to slide continuously from low pass to band pass, to high pass filter.
### Envelope generator
There are two envelope generators : one for the filter, the other for the global sound shape. On the original minimoog, decay can be used (_via_ a switch) to add release to notes. On this one a knob is there for, so this is a classic ADSR envelope.
### LFO
There is a LFO avialble, with continuous rate and waveform selection.
### Modulation
The modulation can be applied to oscillators and or filter, and is controlled by the modulation wheel. Four modulation sources can be used, and mix together. osc.3 or filter envelope can be mixed to noise or LFO.
### Glide (portamento)
Portamento can be from 0 to ten seconds, and switch on and off.
### Octave transpose
There is +- 2 octave transpose available to compensate the only 2 1/2 octave from the keyboard.
### Function settings
There are "hidden" settings available through a function switch. When turn on, the keyboard can be used to change the behavior of some things. Each of this setting is saved when powered off.
#### Keyboard priority mode
This defines the way the keyboard behave. There are four mode available :
1. lower note priority : a note will be played only if it's lower than the one already playing.
1. first note priority : a note will be played only if no note is already playing.
1. last note priority : a note will be played anyway.
1. upper note priority : a note will be played only if it's upper than the one already playing.
In any case, ten notes are tracked, so when several key are pressed releasing a key will play another, according to their position or the order they was pressed.
#### Note retrigger
This defines how a new note is played. If some keys are already pressed when a new note is played, it can retrigger the envelopes, or let them to their state. This can be turned on or off.
#### Keyboard detune
The original minimoog has a resistor ladder keyboard, wich implies small variations in tonality regarding a perfect pitch. This setting reproduce this by applying detune to each note and letting you choice :
1. off : perfect pitch for each key
1. soft : lightly detuned, +-10% of a semitone
1. medium : a bit more detuned, +-30%
1. hard : play drunk, two adjacent notes can almost same the same, +-50%
1. reset : this generate a new random detune table.
The detune table stores a fix detune coefficient for each of all 128 MIDI notes.
#### Bitcrush
Output resolution can be changed. The default is 16 bits, but any bitsize between 4 and 16 can be choose.
The bitcrushing is applied at the end of the audio stream, just before the i2s / USB output.
#### MIDI channel setting
The MIDI channel the synth listens and emit on when connected _via_ USB can be changed. Any channel from 1 to 16.

@ -281,5 +281,3 @@
// #define CC_OMNI_MODE_ON CC125
// #define CC_MONO_MODE_ON CC126
// #define CC_POLY_MODE_ON CC127

@ -84,7 +84,7 @@ const uint8_t NUM_SWITCHES = 15;
const uint8_t NUM_POTS = 16;
const uint8_t NUM_SELECTORS = 6;
const uint8_t POT_FILTER_COEF = 15;
const uint8_t POT_FILTER_COEF = 20;
// Digital pin definition
const uint8_t KEYS[NUM_KEYS] = {
@ -154,7 +154,7 @@ uint16_t mix[5];
bool mixSw[5];
// Misc
uint8_t defaultVelocity = 96;
uint8_t defaultVelocity = 64;
bool update = 0;
@ -171,10 +171,7 @@ MIDI_CREATE_CUSTOM_INSTANCE(HardwareSerial, Serial1, midi1, midiSettings);
void setup(){
// initialisation
midi1.setHandleControlChange(handleControlChange);
midi1.begin(1);
midi1.turnThruOff();
// Serial.begin(115200);
// Key initialisation
for(uint8_t i = 0; i < NUM_KEYS; ++i){
@ -185,7 +182,7 @@ void setup(){
// Switches initialisation
for(uint8_t i = 0; i < NUM_SWITCHES; ++i){
switches[i].begin(PIN[i], INPUT_PULLUP);
switches[i].setDebounceDelay(1);
switches[i].setDebounceDelay(5);
}
// potentiometers initialisation
@ -193,15 +190,80 @@ void setup(){
pots[i].begin(analogRead(APIN[i]));
pots[i].setCoef(POT_FILTER_COEF);
}
/*
// Run init sequence to debounce switches and filter pots, so it's running on stable values.
uint32_t initEnd = millis() + 500;
while(initEnd > millis()){
for(uint8_t i = 0; i < NUM_KEYS; ++i){
keys[i].update();
}
for(uint8_t i = 0; i < NUM_SWITCHES; ++i){
switches[i].update();
}
// This causes the most inexplicable bug I've ever seen.
// When the switches (above) are depressed in the main loop, the board reboots.
// Probably something like a segmentation error, I suspect the board is partially HS.
// For now it will stay the same...
/*
uint16_t value = 0;
for(uint8_t i = 0; i < NUM_POTS; ++i){
uint16_t temp = 0;
value = analogRead(APIN[i]);
temp = pots[i].filter(value);
potState[i] = temp;
}
}
*/
midi1.setHandleControlChange(handleControlChange);
midi1.begin(1);
midi1.turnThruOff();
}
void loop(){
midi1.read();
updateKeys();
updateSwitches();
updateControls();
update = 0;
/*
Serial.println("keys");
for(uint8_t i = 22; i < (22 + NUM_KEYS); ++i){
Serial.print(digitalRead(i));
Serial.print('\t');
}
Serial.println();
Serial.println("pots");
for(uint8_t i = 0; i < NUM_POTS; ++i){
Serial.print(analogRead(APIN[i]));
Serial.print('\t');
}
Serial.println();
Serial.println("switches");
for(uint8_t i = 0; i < NUM_SWITCHES; ++i){
Serial.print(digitalRead(PIN[i]));
Serial.print('\t');
}
Serial.println("\n\n");
delay(50);
*/
}
int32_t remap(int32_t value, int32_t lowerIn, int32_t upperIn, int32_t lowerOut, int32_t upperOut){
int32_t inRange = upperIn - lowerIn;
int32_t outRange = upperOut - lowerOut;
float ratio = (float)outRange / (float)inRange;
value -= lowerIn;
value *= ratio;
value += lowerOut;
if(value < lowerOut) value = lowerOut;
if(value > upperOut) value = upperOut;
return value;
}
void sendLongControlChange(uint8_t controlChange, uint16_t value, uint8_t channel = 1){
@ -223,21 +285,6 @@ void updateMix(uint8_t ch, bool fromSw = 0){
void updateKeys(){
// reading keys
for(uint8_t i = 0; i < NUM_KEYS; ++i){
/*
bool newState = digitalRead(KEYS[i]);
if(newState != keyState[i]){
uint8_t key = i + MIDI_OFFSET;
keyState[i] = newState;
if(newState){
// MIDI note on
midi1.sendNoteOn(key, defaultVelocity, 1);
} else {
// MIDI note off
midi1.sendNoteOff(key, 0, 1);
}
}
*/
keys[i].update();
// uint8_t key = i + MIDI_OFFSET;
if(keys[i].justPressed()){
@ -374,10 +421,14 @@ void updateControls(){
controlChange = CC_PORTAMENTO_TIME;
break;
case 3:
midi1.sendPitchBend((int16_t)value - 512, 1);
// int16_t val = remap(value, 360, 660, -8190, 8190);
midi1.sendPitchBend((int16_t)value, 1);
continue;
case 4:
controlChange = CC_MOD_WHEEL;
value = remap(value, 360, 660, 0, 16384);
// value = map(value, 360, 660, 0, 16384);
// value = constrain(value, 0, 16384);
break;
case 5:
// rotary selector : value must be divided by ~170

@ -125,6 +125,19 @@ void setup(){
pots[i].setCoef(POT_FILTER_COEF);
}
// Run init sequence to debounce switches and filter pots, so it's running for stable values.
uint32_t initEnd = millis() + 500;
while(millis() < initEnd){
for(uint8_t i = 0; i < NUM_SWITCHES; ++i){
switches[i].update();
}
for(uint8_t i = 0; i < NUM_POTS; ++i){
uint16_t value = pots[i].filter(analogRead(APIN[i]));
potState[i] = value;
}
}
midi1.setHandleControlChange(handleControlChange);
midi1.begin(1);
midi1.turnThruOff();

@ -88,9 +88,10 @@ AudioConnection patchCord41(ampPreFilter, 0, vcf, 0);
AudioConnection patchCord42(filterMixer, 0, vcf, 1);
AudioConnection patchCord43(vcf, 0, bandMixer, 0);
AudioConnection patchCord44(vcf, 1, bandMixer, 1);
AudioConnection patchCord45(bandMixer, mainEnvelope);
AudioConnection patchCord46(mainEnvelope, bitCrushOutput);
AudioConnection patchCord47(mainEnvelope, 0, globalMixer, 1);
AudioConnection patchCord48(bitCrushOutput, 0, i2s, 0);
AudioConnection patchCord49(bitCrushOutput, 0, i2s, 1);
AudioConnection patchCord45(vcf, 2, bandMixer, 2);
AudioConnection patchCord46(bandMixer, mainEnvelope);
AudioConnection patchCord47(mainEnvelope, bitCrushOutput);
AudioConnection patchCord48(mainEnvelope, 0, globalMixer, 1);
AudioConnection patchCord49(bitCrushOutput, 0, i2s, 0);
AudioConnection patchCord50(bitCrushOutput, 0, i2s, 1);
// GUItool: end automatically generated code

@ -60,6 +60,7 @@ const uint8_t KEYTRACK_MAX = 10;
// Mega1 sends midi note 0 for the lower note ; we offset it by for octave to get into the usefull range
const uint8_t MIDI_OFFSET = 48;
//const uint8_t MIDI_OFFSET = 0;
const uint8_t NUM_KEYS = 30;
const uint8_t MAX_OCTAVE = 10;
@ -187,6 +188,8 @@ MIDI_CREATE_CUSTOM_INSTANCE(HardwareSerial, Serial4, midi2, midiSettings);
void setup() {
pinMode(13, OUTPUT);
digitalWrite(13, 1);
// Mega resets
pinMode(MEGA1_RST, OUTPUT);
@ -197,8 +200,8 @@ void setup() {
// midi settings, start and callback
midi1.begin(1);
midi1.turnThruOff();
midi1.setHandleNoteOn(handleNoteOn);
midi1.setHandleNoteOff(handleNoteOff);
midi1.setHandleNoteOn(handleInternalNoteOn);
midi1.setHandleNoteOff(handleInternalNoteOff);
midi1.setHandlePitchBend(handlePitchBend);
midi1.setHandleControlChange(handleControlChange);
@ -225,6 +228,10 @@ void setup() {
// TODO : check how to receive and transmit on different channels.
// MIDI.begin(midiInChannel);
// MIDI.turnThruOff();
// MIDI.setHandleNoteOn(handleNoteOn);
// MIDI.setHandleNoteOff(handleNoteOff);
// MIDI.setHandlePitchBend(handlePitchBend);
AudioMemory(200);
@ -302,6 +309,7 @@ void setup() {
bandMixer.gain(0, 1);
bandMixer.gain(1, 0);
bandMixer.gain(2, 0);
// filter
vcf.frequency(FILTER_BASE_FREQUENCY);
@ -326,8 +334,20 @@ void setup() {
bitCrushOutput.bits(16);
bitCrushOutput.sampleRate(44100.0);
delay(500);
delay(1000);
digitalWrite(13, 0);
delay(100);
// Blink. For debug. And letting a bit more time to Mega 1 to start.
for(uint8_t i = 0; i < 5; ++i){
digitalWrite(13, 1);
delay(100);
digitalWrite(13, 0);
delay(50);
}
// Serial.println("asking for all controls");
midi1.sendControlChange(CC_ASK_FOR_DATA, 127, 1);
midi2.sendControlChange(CC_ASK_FOR_DATA, 127, 1);
@ -449,6 +469,14 @@ int8_t keyTrackRemove(uint8_t note){
return update;
}
void handleInternalNoteOn(uint8_t channel, uint8_t note, uint8_t velocity){
if(function){
handleKeyboardFunction(note, 1);
return;
}
handleNoteOn(channel, note + MIDI_OFFSET, velocity);
}
void handleNoteOn(uint8_t channel, uint8_t note, uint8_t velocity){
/*
Serial.print("note ");
@ -456,11 +484,6 @@ void handleNoteOn(uint8_t channel, uint8_t note, uint8_t velocity){
Serial.println(" on");
*/
if(function){
handleKeyboardFunction(note, 1);
return;
}
int8_t newIndex = -1;
int8_t lowerIndex = -1;
int8_t upperIndex = -1;
@ -526,6 +549,13 @@ void handleNoteOn(uint8_t channel, uint8_t note, uint8_t velocity){
}
}
void handleInternalNoteOff(uint8_t channel, uint8_t note, uint8_t velocity){
if(function){
// handleKeyboardFunction(note, 0);
return;
}
handleNoteOff(channel, note + MIDI_OFFSET, velocity);
}
void handleNoteOff(uint8_t channel, uint8_t note, uint8_t velocity){
/*
@ -534,11 +564,6 @@ void handleNoteOff(uint8_t channel, uint8_t note, uint8_t velocity){
Serial.println(" off");
*/
if(function){
// handleKeyboardFunction(note, 0);
return;
}
int8_t lowerIndex = -1;
int8_t upperIndex = -1;
int8_t newIndex = -1;
@ -610,8 +635,8 @@ void handleNoteOff(uint8_t channel, uint8_t note, uint8_t velocity){
}
void handlePitchBend(uint8_t channel, int16_t bend){
dcPitchBend.amplitude(((float)bend - PITCH_BEND_NEUTRAL) / PITCH_BEND_COURSE);
// Pitch bend goes from -168 to 134.
// dcPitchBend.amplitude(((float)bend - PITCH_BEND_NEUTRAL) / PITCH_BEND_COURSE); // Pitch bend goes from -168 to 134.
dcPitchBend.amplitude(((float)bend) / 8190);
// neutral at -11 from up, -24 from down. :/
// MIDI.sendPitchBend(bend - PITCH_BEND_NEUTRAL, 0);
/*
@ -645,7 +670,7 @@ void handleControlChange(uint8_t channel, uint8_t command, uint8_t value){
longValue += value;
/*
Serial.print("value : ");
Serial.print(longValue);
Serial.println(longValue);
Serial.print(" (sent : ");
Serial.print(value);
Serial.println(')');
@ -729,7 +754,8 @@ void handleControlChange(uint8_t channel, uint8_t command, uint8_t value){
break;
case CC_MOD_WHEEL_LSB:
// CC_33
ampModWheel.gain(((float)longValue - 1 - MOD_WHEEL_MIN) / 12 / MOD_WHEEL_COURSE);
// ampModWheel.gain(((float)longValue - 1 - MOD_WHEEL_MIN) / 12 / MOD_WHEEL_COURSE);
ampModWheel.gain(((float)longValue) / 12 / 16384);
// Mod wheel goes from 360 to 666.
/*
Serial.print("mod wheel : ");
@ -782,8 +808,15 @@ void handleControlChange(uint8_t channel, uint8_t command, uint8_t value){
case CC_FILTER_BAND_LSB:
// CC_51
AudioNoInterrupts();
bandMixer.gain(0, ((float)RESO - (float)longValue) / RESO);
bandMixer.gain(1, (float)longValue / RESO);
if(longValue < HALF_RESO){
bandMixer.gain(0, ((float)HALF_RESO - (float)longValue) / HALF_RESO);
bandMixer.gain(1, (float)longValue / HALF_RESO);
bandMixer.gain(2, 0.0);
} else {
bandMixer.gain(0, 0.0);
bandMixer.gain(1, ((float)RESO - (float)longValue) / HALF_RESO);
bandMixer.gain(2, ((float)longValue - HALF_RESO) / HALF_RESO);
}
AudioInterrupts();
break;
case CC_FILTER_CUTOFF_FREQ_LSB:
@ -836,6 +869,10 @@ void handleControlChange(uint8_t channel, uint8_t command, uint8_t value){
break;
case CC_PORTAMENTO_ON_OFF:
// CC_65
/*
Serial.print("portamento on off : ");
Serial.println(value);
*/
if(value < 64){
glideEn = 1;
} else {
@ -1012,8 +1049,6 @@ void handleControlChange(uint8_t channel, uint8_t command, uint8_t value){
void handleKeyboardFunction(uint8_t note, bool active){
// for testing, until the note sent from Mega is changed !
note++;
//
/*
Serial.print("key pressed : ");
@ -1021,53 +1056,44 @@ void handleKeyboardFunction(uint8_t note, bool active){
*/
// Change function
switch(note){
case 48:
case 0:
// lower DO
currentFunction = FUNCTION_KEYBOARD_MODE;
// Serial.println("keyboard mode");
break;
case 50:
case 2:
// lower RE
currentFunction = FUNCTION_RETRIGGER;
// Serial.println("retrigger");
break;
case 52:
case 4:
// lower MI
currentFunction = FUNCTION_DETUNE;
// Serial.println("detune");
break;
case 53:
case 5:
// lower FA
currentFunction = FUNCTION_BITCRUSH;
// Serial.println("bitcrush");
break;
case 55:
case 7:
// lower SOL
currentFunction = FUNCTION_MIDI_IN_CHANNEL;
// Serial.println("midi in channel");
break;
case 57:
case 9:
// lower LA
currentFunction = FUNCTION_MIDI_OUT_CHANNEL;
// Serial.println("midi out channel");
break;
case 59:
case 11:
// lower Si
break;
default:
if(note < 60) return;
note -= 60;
if(note < 12) return;
note -= 12;
break;
}
/*
const uint16_t EE_BITCRUSH_ADD = 0;
const uint16_t EE_KEYBOARD_MODE_ADD = 1;
const uint16_t EE_MIDI_IN_CH_ADD = 2;
const uint16_t EE_MIDI_OUT_CH_ADD = 3;
const uint16_t EE_TRIGGER_ADD = 4;
const uint16_t EE_DETUNE_ADD = 5;
const uint16_t EE_DETUNE_TABLE_ADD = 6;
*/
switch(currentFunction){
case FUNCTION_KEYBOARD_MODE:

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Loading…
Cancel
Save