diff --git a/Open_Theremin_V3/Open_Theremin_V3.ino b/Open_Theremin_V3/Open_Theremin_V3.ino
index 94bedb5..4022591 100644
--- a/Open_Theremin_V3/Open_Theremin_V3.ino
+++ b/Open_Theremin_V3/Open_Theremin_V3.ino
@@ -1,34 +1,38 @@
/*
- * Open.Theremin control software for Arduino UNO
- * Version 3.0
- * Copyright (C) 2010-2016 by Urs Gaudenz
+ * Open Theremin V3 with MIDI interface control software for Arduino UNO
+ * Based on Open Theremin V3 version 3.0 Copyright (C) 2010-2016 by Urs Gaudenz
*
- * Open.Theremin control software is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as published
+ *
+ * Open Theremin V3 with MIDI interface control software 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.
*
- * Open.Theremin control software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * Open Theremin V3 with MIDI interface control software 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
- * the Open.Theremin control software. If not, see .
+ * the Open Theremin V3 with MIDI interface control software.
+ * If not, see .
*
- * With important contributions by
+ * Urs Gaudenz also credits for their important contributions to Open Theremin V3:
* David Harvey
* Michael Margolis
*/
+
+/* Midi added by Vincent Dhamelincourt - September 2017.
+ * Serial com' removed from the original Open Theremin V3 's code for midi purpose.
+ */
/**
Building the code
=================
build.h contains #defines that control the compilation of the code
-ENABLE_SERIAL - if non-0, the build will include code to write the detected
- pitch to the serial connection every 100 milliseconds. Set serial
- receive baud to 115200
ENABLE_CV - if non-0, emit cv output on pin 6 (EXPERIMENTAL!)
@@ -43,6 +47,7 @@ Main application object. Holds the state of the app (playing, calibrating), deal
with initialisation and the app main loop, reads pitch and volume changed flags
from the interrupt handlers and sets pitch and volume values which the timer
interrupt sends to the DAC.
+Midi is also managed here
** OTPinDefs.h **
Pin definitions for the DAC.
@@ -76,4 +81,4 @@ void setup() {
void loop() {
app.loop();
}
-
+
diff --git a/Open_Theremin_V3/application.cpp b/Open_Theremin_V3/application.cpp
index 80dc893..e721653 100644
--- a/Open_Theremin_V3/application.cpp
+++ b/Open_Theremin_V3/application.cpp
@@ -24,15 +24,22 @@ static float qMeasurement = 0;
static int32_t volCalibrationBase = 0;
+static uint16_t old_midi_note =0;
+static uint16_t old_midi_volume =0;
+static uint16_t old_midi_bend = 0;
+
+static double midi_key_follow = 0.5;
+static uint8_t midi_channel = 0;
+static uint8_t midi_bend_range = 0;
+static uint8_t midi_volume_trigger = 0;
+
+
Application::Application()
: _state(PLAYING),
_mode(NORMAL) {
};
void Application::setup() {
-#if SERIAL_ENABLED
- Serial.begin(Application::BAUD);
-#endif
HW_LED1_ON;HW_LED2_OFF;
@@ -58,7 +65,7 @@ initialiseInterrupts();
EEPROM.get(4,pitchCalibrationBase);
EEPROM.get(8,volCalibrationBase);
-
+ midi_setup();
}
@@ -181,62 +188,73 @@ void Application::loop() {
vWavetableSelector=wavePotValueL>>7;
registerValue=4-(registerPotValueL>>8);
- if (_state == PLAYING && HW_BUTTON_PRESSED) {
+ if (_state == PLAYING && HW_BUTTON_PRESSED)
+ {
_state = CALIBRATING;
+ _midistate = MIDI_STOP;
+
resetTimer();
}
- if (_state == CALIBRATING && HW_BUTTON_RELEASED) {
- if (timerExpired(1500)) {
-
+ if (_state == CALIBRATING && HW_BUTTON_RELEASED)
+ {
+ if (timerExpired(1500))
+ {
_mode = nextMode();
- if (_mode==NORMAL) {HW_LED1_ON;HW_LED2_OFF;} else {HW_LED1_OFF;HW_LED2_ON;};
- // playModeSettingSound();
-
-
+ if (_mode==NORMAL)
+ {
+ HW_LED1_ON;HW_LED2_OFF;
+ _midistate = MIDI_SILENT;
+ }
+ else
+ {
+ HW_LED1_OFF;HW_LED2_ON;
+ };
+ // playModeSettingSound();
}
_state = PLAYING;
};
- if (_state == CALIBRATING && timerExpired(15000)) {
-
- HW_LED2_ON;
+ if (_state == CALIBRATING && timerExpired(15000))
+ {
+ HW_LED2_ON;
- playStartupSound();
-
- calibrate_pitch();
- calibrate_volume();
+ playStartupSound();
+ if (registerPotValue < 512) // if register pot turned CCW
+ {
+ // calibrate heterodyne parameters
+ calibrate_pitch();
+ calibrate_volume();
- initialiseTimer();
- initialiseInterrupts();
+
+ initialiseTimer();
+ initialiseInterrupts();
- playCalibratingCountdownSound();
- calibrate();
-
+ playCalibratingCountdownSound();
+ calibrate();
+ }
+ else // if register turned CW
+ {
+ // calibrate midi parameters
+ midi_calibrate ();
+ };
-
-
-
- _mode=NORMAL;
- HW_LED2_OFF;
+ _mode=NORMAL;
+ HW_LED2_OFF;
while (HW_BUTTON_PRESSED)
; // NOP
+
_state = PLAYING;
+ _midistate = MIDI_SILENT;
};
#if CV_ENABLED
OCR0A = pitch & 0xff;
#endif
-#if SERIAL_ENABLED
- if (timerExpired(TICKS_100_MILLIS)) {
- resetTimer();
- Serial.write(pitch & 0xff); // Send char on serial (if used)
- Serial.write((pitch >> 8) & 0xff);
- }
-#endif
+
if (pitchValueAvailable) { // If capture event
@@ -281,6 +299,12 @@ void Application::loop() {
volumeValueAvailable = false;
}
+ if (midi_timer > 100) // run midi app every 100 ticks equivalent to approximatevely 3 ms to avoid synth's overload
+ {
+ midi_application ();
+ midi_timer = 0;
+ }
+
goto mloop; // End of main loop
}
@@ -319,8 +343,6 @@ static long pitchfn0 = 0;
static long pitchfn1 = 0;
static long pitchfn = 0;
- Serial.begin(115200);
- Serial.println("\nPITCH CALIBRATION\n");
HW_LED1_OFF;
HW_LED2_ON;
@@ -330,8 +352,6 @@ static long pitchfn = 0;
mcpDacInit();
qMeasurement = GetQMeasurement(); // Measure Arudino clock frequency
- Serial.print("Arudino Freq: ");
- Serial.println(qMeasurement);
q0 = (16000000/qMeasurement*500000); //Calculated set frequency based on Arudino clock frequency
@@ -340,8 +360,6 @@ pitchXn1 = 4095;
pitchfn = q0-PitchFreqOffset; // Add offset calue to set frequency
-Serial.print("\nPitch Set Frequency: ");
-Serial.println(pitchfn);
mcpDac2BSend(1600);
@@ -354,11 +372,6 @@ mcpDac2ASend(pitchXn1);
delay(100);
pitchfn1 = GetPitchMeasurement();
-Serial.print ("Frequency tuning range: ");
-Serial.print(pitchfn0);
-Serial.print(" to ");
-Serial.println(pitchfn1);
-
while(abs(pitchfn0-pitchfn1)>CalibrationTolerance){ // max allowed pitch frequency offset
@@ -372,14 +385,6 @@ pitchfn1 = GetPitchMeasurement()-pitchfn;
pitchXn2=pitchXn1-((pitchXn1-pitchXn0)*pitchfn1)/(pitchfn1-pitchfn0); // new DAC value
-Serial.print("\nDAC value L: ");
-Serial.print(pitchXn0);
-Serial.print(" Freq L: ");
-Serial.println(pitchfn0);
-Serial.print("DAC value H: ");
-Serial.print(pitchXn1);
-Serial.print(" Freq H: ");
-Serial.println(pitchfn1);
pitchXn0 = pitchXn1;
@@ -406,8 +411,6 @@ static long volumefn0 = 0;
static long volumefn1 = 0;
static long volumefn = 0;
- Serial.begin(115200);
- Serial.println("\nVOLUME CALIBRATION");
InitialiseVolumeMeasurement();
interrupts();
@@ -420,8 +423,6 @@ volumeXn1 = 4095;
q0 = (16000000/qMeasurement*460765);
volumefn = q0-VolumeFreqOffset;
-Serial.print("\nVolume Set Frequency: ");
-Serial.println(volumefn);
mcpDac2BSend(volumeXn0);
@@ -435,11 +436,6 @@ delay_NOP(44316);//44316=100ms
volumefn1 = GetVolumeMeasurement();
-Serial.print ("Frequency tuning range: ");
-Serial.print(volumefn0);
-Serial.print(" to ");
-Serial.println(volumefn1);
-
while(abs(volumefn0-volumefn1)>CalibrationTolerance){
@@ -453,14 +449,6 @@ volumefn1 = GetVolumeMeasurement()-volumefn;
volumeXn2=volumeXn1-((volumeXn1-volumeXn0)*volumefn1)/(volumefn1-volumefn0); // calculate new DAC value
-Serial.print("\nDAC value L: ");
-Serial.print(volumeXn0);
-Serial.print(" Freq L: ");
-Serial.println(volumefn0);
-Serial.print("DAC value H: ");
-Serial.print(volumeXn1);
-Serial.print(" Freq H: ");
-Serial.println(volumefn1);
volumeXn0 = volumeXn1;
@@ -474,7 +462,6 @@ EEPROM.put(2,volumeXn0);
HW_LED2_OFF;
HW_LED1_ON;
- Serial.println("\nCALIBRATION COMPTLETED\n");
}
void Application::hzToAddVal(float hz) {
@@ -515,4 +502,247 @@ void Application::delay_NOP(unsigned long time) {
+void Application::midi_setup()
+{
+
+ EEPROM.get(12,midi_channel);
+ EEPROM.get(13,midi_bend_range);
+ EEPROM.get(14,midi_volume_trigger);
+
+ // Set MIDI baud rate:
+ Serial.begin(115200); // Baudrate for midi to serial. Use a serial to midi router http://projectgus.github.com/hairless-midiserial/
+ //Serial.begin(31250); // Baudrate for real midi. Use din connection https://www.arduino.cc/en/Tutorial/Midi or HIDUINO https://github.com/ddiakopoulos/hiduino
+
+ _midistate = MIDI_SILENT;
+}
+
+
+void Application::midi_msg_send(uint8_t channel, uint8_t midi_cmd1, uint8_t midi_cmd2, uint8_t midi_value)
+{
+ uint8_t mixed_cmd1_channel;
+
+ mixed_cmd1_channel = (midi_cmd1 & 0xF0)| (channel & 0x0F);
+
+ Serial.write(mixed_cmd1_channel);
+ Serial.write(midi_cmd2);
+ Serial.write(midi_value);
+}
+
+// midi_application sends note and volume and uses pitch bend to simulate continuous picth.
+// Calibrate pitch bend and other parameters accordingly to the receiver synth (see midi_calibrate).
+// New notes won't be generated as long as pitch bend will do the job.
+// The bigger is synth's pitch bend range the beter is the effect.
+// If pitch bend range = 1 no picth bend is generated (portamento will do a better job)
+void Application::midi_application ()
+{
+ uint16_t new_midi_note;
+ uint16_t new_midi_volume;
+
+ uint16_t new_midi_bend;
+ uint8_t midi_bend_low;
+ uint8_t midi_bend_high;
+
+ double double_log_freq;
+ double double_log_bend;
+
+ // Calculate volume for midi
+ new_midi_volume = vScaledVolume >> 1;
+ new_midi_volume = min (new_midi_volume, 127);
+
+ // Calculate note and pitch bend for midi
+ if (vPointerIncrement < 18)
+ {
+ // Highest note
+ new_midi_note = 0;
+ new_midi_bend = 8192;
+ }
+ else if (vPointerIncrement > 26315)
+ {
+ // Lowest note
+ new_midi_note = 127;
+ new_midi_bend = 8192;
+ }
+ else
+ {
+ // Find note in the playing range
+ double_log_freq = (log (vPointerIncrement/17.152) / 0.057762265); // Precise note played in the logaritmic scale
+ double_log_bend = double_log_freq - old_midi_note; // How far from last played midi chromatic note we are
+
+ // If too much far from last midi chromatic note played (midi_key_follow depends on pitch bend range)
+ if (abs (double_log_bend) >= midi_key_follow)
+ {
+ new_midi_note = round (double_log_freq); // Select the new midi chromatic note
+ double_log_bend = double_log_freq - new_midi_note; // calculate bend to reach precise note played
+ }
+ else
+ {
+ new_midi_note = old_midi_note; // No change
+ }
+
+ // If pitch bend range greater than 1
+ if (midi_bend_range > 1)
+ {
+ // use it to reach precise note played
+ new_midi_bend = 8192 + (8191 * double_log_bend / midi_bend_range); // Calculate midi pitch bend
+ }
+ else
+ {
+ // Don't use pitch bend (portamento would do a beter job)
+ new_midi_bend = 8192;
+ }
+ }
+
+ // Prepare the 2 bites of picth bend midi message
+ midi_bend_low = (int8_t) (new_midi_bend & 0x007F);
+ midi_bend_high = (int8_t) ((new_midi_bend & 0x3F80)>> 7);
+
+ // State machine for MIDI
+ switch (_midistate)
+ {
+ case MIDI_SILENT:
+ // If player's hand moves away from volume antenna
+ if (new_midi_volume > midi_volume_trigger)
+ {
+ // Send pitch bend to reach precise played note (send 8192 (no pitch bend) in case of midi_bend_range == 1)
+ midi_msg_send(midi_channel, 0xE0, midi_bend_low, midi_bend_high);
+ old_midi_bend = new_midi_bend;
+
+ // Send volume to reach precise played volume
+ midi_msg_send(midi_channel, 0xB0, 0x07, new_midi_volume);
+ old_midi_volume = new_midi_volume;
+
+ // Play the note
+ midi_msg_send(midi_channel, 0x90, new_midi_note, 0x45);
+ old_midi_note = new_midi_note;
+
+ // Set key follow so as next played note will be at limit of pitch bend range
+ midi_key_follow = (double)(midi_bend_range) - 0.5;
+
+ _midistate = MIDI_PLAYING;
+ }
+ else
+ {
+ // Do nothing
+ }
+ break;
+
+ case MIDI_PLAYING:
+ // If player's hand is far from volume antenna
+ if (new_midi_volume > midi_volume_trigger)
+ {
+ // Refresh midi pitch bend value
+ if (new_midi_bend != old_midi_bend)
+ {
+ midi_msg_send(midi_channel, 0xE0, midi_bend_low, midi_bend_high);
+ old_midi_bend = new_midi_bend;
+ }
+ else
+ {
+ // do nothing
+ }
+
+ // Refresh midi volume value
+ if (new_midi_volume != old_midi_volume)
+ {
+ midi_msg_send(midi_channel, 0xB0, 0x07, new_midi_volume);
+ old_midi_volume = new_midi_volume;
+ }
+ else
+ {
+ // do nothing
+ }
+
+ // Refresh midi note
+ if (new_midi_note != old_midi_note)
+ {
+ // Play new note before muting old one to play legato on monophonic synth
+ // (pitch pend management tends to break expected effect here)
+ midi_msg_send(midi_channel, 0x90, new_midi_note, 0x45);
+ midi_msg_send(midi_channel, 0x90, old_midi_note, 0);
+ old_midi_note = new_midi_note;
+ }
+ else
+ {
+ // do nothing
+ }
+ }
+ else // Means that player's hand moves to the volume antenna
+ {
+ // Send volume = 0
+ midi_msg_send(midi_channel, 0xB0, 0x07, 0);
+ old_midi_volume = 0;
+
+ // Send note off
+ midi_msg_send(midi_channel, 0x90, old_midi_note, 0);
+
+ // Set key follow to the minimum in order to use closest note played as the center note for pitch bend next time
+ midi_key_follow = 0.5;
+
+ _midistate = MIDI_SILENT;
+ }
+ break;
+
+ case MIDI_STOP:
+ // Send all note off
+ midi_msg_send(midi_channel, 0xB0, 0x7B, 0x00);
+
+ _midistate = MIDI_MUTE;
+ break;
+
+ case MIDI_MUTE:
+ //do nothing
+ break;
+
+ }
+}
+
+// midi_calibrate allows the user to set some midi parameters
+// Set potentiometer accordingly to comments bellow BEFORE entering in midi calibration mode.
+// Hear may help somewhat to determine entered values
+void Application::midi_calibrate ()
+{
+ uint16_t pot_channel;
+ uint16_t pot_bend_range;
+ uint16_t pot_volume_trigger;
+
+ uint16_t bend_range_scale;
+
+ // Midi channel uses "Timbre" pot.
+ // Waveform may help user do determine which couple of channel is chosen (WF 1 Lo -> Ch1, WF 1 Hi -> Ch2, WF 2 Lo -> Ch3, etc...)
+ pot_channel = analogRead(WAVE_SELECT_POT);
+ midi_channel = (uint8_t)((pot_channel >> 6) & 0x000F);
+ EEPROM.put(12,midi_channel);
+
+
+ // Pitch bend range and associated distance between notes jumps use "Pitch" pot.
+ // The user shall set synth's pitch bend range acordingly to the selected Theremin's pitch bend range:
+ // 1 semitone, 7 semitones (a fifth), 12 semitones (an octave) or 24 semitones (two octaves).
+ // The "1 semitone" setting blocks pitch bend generation (use portamento on the synth)
+ pot_bend_range = analogRead(PITCH_POT);
+ bend_range_scale = pot_bend_range >> 8;
+ if (bend_range_scale == 0)
+ {
+ midi_bend_range = 1;
+ }
+ else if (bend_range_scale == 1)
+ {
+ midi_bend_range = 7;
+ }
+ else if (bend_range_scale == 2)
+ {
+ midi_bend_range = 12;
+ }
+ else
+ {
+ midi_bend_range = 24;
+ }
+ EEPROM.put(13,midi_bend_range);
+
+ // Volume trigger uses "Volume" pot
+ // Select a high value if some percussive sounds are played (so as it is heard when volume is not null)
+ pot_volume_trigger = analogRead(VOLUME_POT);
+ midi_volume_trigger = (uint8_t)((pot_volume_trigger >> 3) & 0x007F);
+ EEPROM.put(14,midi_volume_trigger);
+}
+
diff --git a/Open_Theremin_V3/application.h b/Open_Theremin_V3/application.h
index 92fa963..659576d 100644
--- a/Open_Theremin_V3/application.h
+++ b/Open_Theremin_V3/application.h
@@ -7,6 +7,7 @@
enum AppState {CALIBRATING = 0, PLAYING};
enum AppMode {MUTE = 0, NORMAL};
+enum AppMidiState {MIDI_SILENT = 0, MIDI_PLAYING, MIDI_STOP, MIDI_MUTE};
class Application {
public:
@@ -32,12 +33,10 @@ class Application {
-#if SERIAL_ENABLED
- static const int BAUD = 115200;
-#endif
AppState _state;
AppMode _mode;
+ AppMidiState _midistate;
void calibrate();
void calibrate_pitch();
@@ -64,6 +63,12 @@ class Application {
void playCalibratingCountdownSound();
void playModeSettingSound();
void delay_NOP(unsigned long time);
+
+ void midi_setup();
+ void midi_msg_send(uint8_t channel, uint8_t midi_cmd1, uint8_t midi_cmd2, uint8_t midi_value);
+ void midi_application ();
+ void midi_calibrate ();
+
};
-#endif // _APPLICATION_H
+#endif // _APPLICATION_H
diff --git a/Open_Theremin_V3/build.h b/Open_Theremin_V3/build.h
index bfa8e72..9a7c8dc 100644
--- a/Open_Theremin_V3/build.h
+++ b/Open_Theremin_V3/build.h
@@ -4,11 +4,9 @@
#define _BUILD_H
-// Set to build with serial support
-#define SERIAL_ENABLED 0
// Set to build with control voltage output (experimental)
#define CV_ENABLED 0
-#endif // _BUILD_H
+#endif // _BUILD_H
diff --git a/Open_Theremin_V3/ihandlers.cpp b/Open_Theremin_V3/ihandlers.cpp
index 1cc16bc..a4da417 100644
--- a/Open_Theremin_V3/ihandlers.cpp
+++ b/Open_Theremin_V3/ihandlers.cpp
@@ -163,6 +163,7 @@ ISR (INT1_vect) {
#endif //CV play sound
incrementTimer(); // update 32us timer
+ incrementMidiTimer(); // update 32us miditimer
if (PC_STATE) debounce_p++;
if (debounce_p == 3) {
@@ -217,4 +218,4 @@ ISR(TIMER1_OVF_vect)
timer_overflow_counter++;
}
-
+
diff --git a/Open_Theremin_V3/timer.cpp b/Open_Theremin_V3/timer.cpp
index 77a95cc..e3b63b1 100644
--- a/Open_Theremin_V3/timer.cpp
+++ b/Open_Theremin_V3/timer.cpp
@@ -3,6 +3,7 @@
#include "timer.h"
volatile uint16_t timer = 0;
+volatile uint16_t midi_timer = 0;
void ticktimer (uint16_t ticks) {
resetTimer();
@@ -14,4 +15,4 @@ void millitimer (uint16_t milliseconds) {
ticktimer(millisToTicks(milliseconds));
}
-
+
diff --git a/Open_Theremin_V3/timer.h b/Open_Theremin_V3/timer.h
index 5f05ede..0abb22e 100644
--- a/Open_Theremin_V3/timer.h
+++ b/Open_Theremin_V3/timer.h
@@ -2,6 +2,7 @@
#define _TIMER_H
extern volatile uint16_t timer;
+extern volatile uint16_t midi_timer;
inline uint16_t millisToTicks(uint16_t milliseconds) {
return milliseconds * (1000.0f/32);
@@ -15,6 +16,10 @@ inline void incrementTimer() {
timer++;
}
+inline void incrementMidiTimer() {
+ midi_timer++;
+}
+
inline bool timerExpired(uint16_t ticks) {
return timer >= ticks;
}
@@ -34,8 +39,5 @@ inline bool timerUnexpiredMillis(uint16_t milliseconds) {
void ticktimer (uint16_t ticks);
void millitimer (uint16_t milliseconds);
-#if SERIAL_ENABLED
-const uint16_t TICKS_100_MILLIS = millisToTicks(100);
-#endif //SERIAL_ENABLED
#endif // _TIMER_H
diff --git a/README.md b/README.md
index 7914b33..0a13481 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
-## Open.Theremin V3 control software
+## Open Theremin V3 with MIDI interface control software for Arduino UNO
-Arduino UNO Software for the Open.Theremin
+Based on Arduino UNO Software for the Open.Theremin version 3.0 Copyright (C) 2010-2016 by Urs Gaudenz
+https://github.com/GaudiLabs/OpenTheremin_V3
### Don't click on the files!
Click on the "Download ZIP" Button to the right or [Click here](https://github.com/GaudiLabs/OpenTheremin_V3/archive/master.zip)