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