diff --git a/software/o_c_REV/APP_HEMISPHERE.ino b/software/o_c_REV/APP_HEMISPHERE.ino index 39537ffc1..f16645abd 100644 --- a/software/o_c_REV/APP_HEMISPHERE.ino +++ b/software/o_c_REV/APP_HEMISPHERE.ino @@ -54,7 +54,7 @@ typedef struct Applet { void (*OnDataReceive)(bool, uint64_t); // Send a data int to the applet } Applet; -// The settings specify the selected applets, and 32 bits of data for each applet +// The settings specify the selected applets, and 64 bits of data for each applet enum HEMISPHERE_SETTINGS { HEMISPHERE_SELECTED_LEFT_ID, HEMISPHERE_SELECTED_RIGHT_ID, @@ -66,7 +66,8 @@ enum HEMISPHERE_SETTINGS { HEMISPHERE_RIGHT_DATA_B3, HEMISPHERE_LEFT_DATA_B4, HEMISPHERE_RIGHT_DATA_B4, - HEMISPHERE_CLOCK_DATA, + HEMISPHERE_CLOCK_DATA1, + HEMISPHERE_CLOCK_DATA2, HEMISPHERE_SETTING_LAST }; @@ -103,7 +104,8 @@ public: (uint64_t(values_[2 + h])); available_applets[index].OnDataReceive(h, data); } - ClockSetup.OnDataReceive(0, uint64_t(values_[HEMISPHERE_CLOCK_DATA])); + ClockSetup.OnDataReceive(0, (uint64_t(values_[HEMISPHERE_CLOCK_DATA2]) << 16) | + uint64_t(values_[HEMISPHERE_CLOCK_DATA1])); } void SetApplet(int hemisphere, int index) { @@ -136,7 +138,12 @@ public: } } - if (clock_setup) ClockSetup.Controller(LEFT_HEMISPHERE, clock_m->IsForwarded()); + // Advance internal clock, sync to external pulse + if (clock_m->IsRunning()) + clock_m->SyncTrig( OC::DigitalInputs::clocked() ); + + // NJM: always execute ClockSetup controller - it handles MIDI clock out + ClockSetup.Controller(LEFT_HEMISPHERE, clock_m->IsForwarded()); for (int h = 0; h < 2; h++) { @@ -187,10 +194,13 @@ public: } void DelegateSelectButtonPush(int hemisphere) { - if (OC::CORE::ticks - click_tick < HEMISPHERE_DOUBLE_CLICK_TIME && hemisphere == first_click) { + if (OC::CORE::ticks - click_tick < HEMISPHERE_DOUBLE_CLICK_TIME) { // This is a double-click, so activate corresponding help screen, leave // Select Mode, and reset the double-click timer - SetHelpScreen(hemisphere); + if (hemisphere == first_click) + SetHelpScreen(hemisphere); + else // up + down simultaneous + clock_setup = 1; select_mode = -1; click_tick = 0; } else { @@ -203,12 +213,13 @@ public: // If we're in the clock setup screen, we want to exit the setup without turning on Select Mode if (hemisphere == select_mode) select_mode = -1; // Leave Select Mode is same button is pressed else select_mode = hemisphere; // Otherwise, set Select Mode - click_tick = OC::CORE::ticks; } + click_tick = OC::CORE::ticks; first_click = hemisphere; } - clock_setup = 0; // Turn off clock setup with any button press + if (click_tick) + clock_setup = 0; // Turn off clock setup with any single button press } void DelegateEncoderMovement(const UI::Event &event) { @@ -257,7 +268,9 @@ public: apply_value(6 + h, (data >> 32) & 0xffff); apply_value(8 + h, (data >> 48) & 0xffff); } - apply_value(HEMISPHERE_CLOCK_DATA, ClockSetup.OnDataRequest(0)); + uint64_t data = ClockSetup.OnDataRequest(0); + apply_value(HEMISPHERE_CLOCK_DATA1, data & 0xffff); + apply_value(HEMISPHERE_CLOCK_DATA2, (data >> 16) & 0xffff); } void OnSendSysEx() { @@ -362,7 +375,8 @@ SETTINGS_DECLARE(HemisphereManager, HEMISPHERE_SETTING_LAST) { {0, 0, 65535, "Data R block 3", NULL, settings::STORAGE_TYPE_U16}, {0, 0, 65535, "Data L block 4", NULL, settings::STORAGE_TYPE_U16}, {0, 0, 65535, "Data R block 4", NULL, settings::STORAGE_TYPE_U16}, - {0, 0, 65535, "Clock data", NULL, settings::STORAGE_TYPE_U16} + {0, 0, 65535, "Clock data 1", NULL, settings::STORAGE_TYPE_U16}, + {0, 0, 65535, "Clock data 2", NULL, settings::STORAGE_TYPE_U16} }; HemisphereManager manager; diff --git a/software/o_c_REV/HEM_ClockSetup.ino b/software/o_c_REV/HEM_ClockSetup.ino index 44fc2ef5a..1ad8b3a9a 100644 --- a/software/o_c_REV/HEM_ClockSetup.ino +++ b/software/o_c_REV/HEM_ClockSetup.ino @@ -21,63 +21,113 @@ class ClockSetup : public HemisphereApplet { public: + enum ClockSetupCursor { + PLAY_STOP, + FORWARDING, + EXT_PPQN, + TEMPO, + MULT1, + MULT2, + TRIG1, + TRIG2, + TRIG3, + TRIG4, + LAST_SETTING = TRIG4 + }; + const char* applet_name() { return "ClockSet"; } void Start() { } - // When the ClockSetup is active, the selected applets should continue to function, so - // there's no need to have a controller for ClockSetup. - void Controller() { } + // The ClockSetup controller handles MIDI Clock and Transport Start/Stop + void Controller() { + if (start_q){ + start_q = 0; + usbMIDI.sendRealTime(usbMIDI.Start); + } + if (stop_q){ + stop_q = 0; + usbMIDI.sendRealTime(usbMIDI.Stop); + } + if (clock_m->IsRunning() && clock_m->MIDITock()) usbMIDI.sendRealTime(usbMIDI.Clock); + } void View() { DrawInterface(); } void OnButtonPress() { - if (++cursor > 2) cursor = 0; + if (!EditMode()) { // special cases for toggle buttons + if (cursor == PLAY_STOP) PlayStop(); + else if (cursor == FORWARDING) clock_m->ToggleForwarding(); + else if (cursor >= TRIG1) clock_m->Boop(cursor-TRIG1); + else CursorAction(cursor, LAST_SETTING); + } + else CursorAction(cursor, LAST_SETTING); } void OnEncoderMove(int direction) { - if (cursor == 0) { // Source - if (clock_m->IsRunning() || clock_m->IsPaused()) clock_m->Stop(); - else { - clock_m->Start(); - clock_m->Pause(); - } - } - - if (cursor == 1) { // Set tempo - uint16_t bpm = clock_m->GetTempo(); - bpm += direction; - clock_m->SetTempoBPM(bpm); + if (!EditMode()) { + MoveCursor(cursor, direction, LAST_SETTING); + return; } - if (cursor == 2) { // Set multiplier - int8_t mult = clock_m->GetMultiply(); - mult += direction; - clock_m->SetMultiply(mult); + switch ((ClockSetupCursor)cursor) { + case PLAY_STOP: + PlayStop(); + break; + + case FORWARDING: + clock_m->ToggleForwarding(); + break; + + case TRIG1: + case TRIG2: + case TRIG3: + case TRIG4: + clock_m->Boop(cursor-TRIG1); + break; + + case EXT_PPQN: + clock_m->SetClockPPQN(clock_m->GetClockPPQN() + direction); + break; + case TEMPO: + clock_m->SetTempoBPM(clock_m->GetTempo() + direction); + break; + + case MULT1: + case MULT2: + clock_m->SetMultiply(clock_m->GetMultiply(cursor - MULT1) + direction, cursor - MULT1); + break; + + default: break; } } uint64_t OnDataRequest() { uint64_t data = 0; Pack(data, PackLocation { 0, 1 }, clock_m->IsRunning() || clock_m->IsPaused()); - Pack(data, PackLocation { 1, 9 }, clock_m->GetTempo()); - Pack(data, PackLocation { 10, 5 }, clock_m->GetMultiply()); + Pack(data, PackLocation { 1, 1 }, clock_m->IsForwarded()); + Pack(data, PackLocation { 2, 9 }, clock_m->GetTempo()); + Pack(data, PackLocation { 11, 6 }, clock_m->GetMultiply(0)+32); + Pack(data, PackLocation { 17, 6 }, clock_m->GetMultiply(1)+32); + Pack(data, PackLocation { 23, 5 }, clock_m->GetClockPPQN()); return data; } void OnDataReceive(uint64_t data) { if (Unpack(data, PackLocation { 0, 1 })) { - clock_m->Start(); - clock_m->Pause(); + clock_m->Start(1); // start paused } else { clock_m->Stop(); } - clock_m->SetTempoBPM(Unpack(data, PackLocation { 1, 9 })); - clock_m->SetMultiply(Unpack(data, PackLocation { 10, 5 })); + clock_m->SetForwarding(Unpack(data, PackLocation { 1, 1 })); + clock_m->SetTempoBPM(Unpack(data, PackLocation { 2, 9 })); + clock_m->SetMultiply(Unpack(data, PackLocation { 11, 6 })-32,0); + clock_m->SetMultiply(Unpack(data, PackLocation { 17, 6 })-32,1); + clock_m->SetClockPPQN(Unpack(data, PackLocation { 23, 5 })); } protected: @@ -91,44 +141,89 @@ protected: } private: - int cursor; // 0=Source, 1=Tempo, 2=Multiply + int cursor; // ClockSetupCursor + bool start_q; + bool stop_q; ClockManager *clock_m = clock_m->get(); + void PlayStop() { + if (clock_m->IsRunning()) { + stop_q = 1; + clock_m->Stop(); + } else { + start_q = 1; + clock_m->Start(); + } + } + void DrawInterface() { // Header: This is sort of a faux applet, so its header // needs to extend across the screen graphics.setPrintPos(1, 2); graphics.print("Clock Setup"); - gfxLine(0, 10, 62, 10); - gfxLine(0, 12, 62, 12); + //gfxLine(0, 10, 62, 10); + //gfxLine(0, 12, 62, 12); graphics.drawLine(0, 10, 127, 10); graphics.drawLine(0, 12, 127, 12); // Clock Source + gfxIcon(1, 15, CLOCK_ICON); if (clock_m->IsRunning()) { - gfxIcon(1, 15, PLAY_ICON); - gfxPrint(16, 15, "Internal"); + gfxIcon(12, 15, PLAY_ICON); } else if (clock_m->IsPaused()) { - gfxIcon(1, 15, PAUSE_ICON); - gfxPrint(16, 15, "Internal"); + gfxIcon(12, 15, PAUSE_ICON); } else { - gfxIcon(1, 15, CLOCK_ICON); - gfxPrint(16, 15, "Forward"); + gfxIcon(12, 15, STOP_ICON); } + gfxPrint(26, 15, "Fwd "); + gfxIcon(50, 15, clock_m->IsForwarded() ? CHECK_ON_ICON : CHECK_OFF_ICON); + + // Input PPQN + gfxPrint(64, 15, "PPQN x"); + gfxPrint(clock_m->GetClockPPQN()); // Tempo - gfxIcon(1, 25, NOTE4_ICON); - gfxPrint(9, 25, "= "); + gfxIcon(1, 26, NOTE4_ICON); + gfxPrint(9, 26, "= "); gfxPrint(pad(100, clock_m->GetTempo()), clock_m->GetTempo()); gfxPrint(" BPM"); // Multiply - gfxPrint(1, 35, "x"); - gfxPrint(clock_m->GetMultiply()); + ForEachChannel(ch) { + int mult = clock_m->GetMultiply(ch); + gfxPrint(1 + ch*64, 37, (mult >= 0) ? "x" : "/"); + gfxPrint( (mult >= 0) ? mult : 1 - mult ); + } + + // Manual triggers + for (int i=0; i<4; i++) { gfxIcon(4 + i*32, 49, BURST_ICON); } - if (cursor == 0) gfxCursor(16, 23, 46); - if (cursor == 1) gfxCursor(23, 33, 18); - if (cursor == 2) gfxCursor(8, 43, 12); + switch ((ClockSetupCursor)cursor) { + case PLAY_STOP: gfxFrame(11, 14, 10, 10); break; + case FORWARDING: gfxFrame(49, 14, 10, 10); break; + + case EXT_PPQN: + gfxCursor(100,23, 12); + break; + + case TEMPO: + gfxCursor(22, 34, 18); + break; + + case MULT1: + case MULT2: + gfxCursor(8 + 64*(cursor-MULT1), 45, 12); + break; + + case TRIG1: + case TRIG2: + case TRIG3: + case TRIG4: + gfxFrame(3 + 32*(cursor-TRIG1), 48, 10, 10); + break; + + default: break; + } } }; diff --git a/software/o_c_REV/HEM_Metronome.ino b/software/o_c_REV/HEM_Metronome.ino index a551698a0..41aacf20f 100644 --- a/software/o_c_REV/HEM_Metronome.ino +++ b/software/o_c_REV/HEM_Metronome.ino @@ -34,9 +34,9 @@ public: // Outputs if (clock_m->IsRunning()) { - if (clock_m->Tock()) { + if (clock_m->Tock(hemisphere)) { ClockOut(0); - if (clock_m->EndOfBeat()) ClockOut(1); + if (clock_m->EndOfBeat(hemisphere)) ClockOut(1); } } } @@ -49,21 +49,14 @@ public: void OnButtonPress() { } void OnEncoderMove(int direction) { - uint16_t bpm = clock_m->GetTempo(); - bpm += direction; - clock_m->SetTempoBPM(bpm); + clock_m->SetTempoBPM(clock_m->GetTempo() + direction); } uint64_t OnDataRequest() { - uint64_t data = 0; - Pack(data, PackLocation {0,16}, clock_m->GetTempo()); - Pack(data, PackLocation {16,5}, clock_m->GetMultiply() - 1); - return data; + return 0; } void OnDataReceive(uint64_t data) { - clock_m->SetTempoBPM(Unpack(data, PackLocation {0,16})); - clock_m->SetMultiply(Unpack(data, PackLocation {16,5}) + 1); } protected: @@ -77,7 +70,6 @@ protected: } private: - int cursor; // 0=Tempo, 1=Multiply, 2=Start/Stop ClockManager *clock_m = clock_m->get(); void DrawInterface() { diff --git a/software/o_c_REV/HSClockManager.h b/software/o_c_REV/HSClockManager.h index 8b7734740..c28cb4ee9 100644 --- a/software/o_c_REV/HSClockManager.h +++ b/software/o_c_REV/HSClockManager.h @@ -24,34 +24,45 @@ #ifndef CLOCK_MANAGER_H #define CLOCK_MANAGER_H -const uint16_t CLOCK_TEMPO_MIN = 10; -const uint16_t CLOCK_TEMPO_MAX = 300; +#define CLOCK_PPQN 4 + +static constexpr uint16_t CLOCK_TEMPO_MIN = 10; +static constexpr uint16_t CLOCK_TEMPO_MAX = 300; +static constexpr uint32_t CLOCK_TICKS_MIN = 1000000 / CLOCK_TEMPO_MAX; +static constexpr uint32_t CLOCK_TICKS_MAX = 1000000 / CLOCK_TEMPO_MIN; + +constexpr int MIDI_OUT_PPQN = 24; +constexpr int CLOCK_MAX_MULTIPLE = 24; +constexpr int CLOCK_MIN_MULTIPLE = -31; // becomes /32 class ClockManager { static ClockManager *instance; - uint32_t ticks_per_tock; // Based on the selected tempo in BPM - uint32_t last_tock_tick; // The tick of the most recent tock - uint32_t last_tock_check; // To avoid checking the tock more than once per tick - bool tock; // The most recent tock value + + enum ClockOutput { + LEFT_CLOCK, + RIGHT_CLOCK, + MIDI_CLOCK, + NR_OF_CLOCKS + }; + uint16_t tempo; // The set tempo, for display somewhere else - bool running; // Specifies whether the clock is running for interprocess communication - bool paused; // Specifies whethr the clock is paused - int8_t tocks_per_beat; // Multiplier - bool cycle; // Alternates for each tock, for display purposes - byte count; // Multiple counter - bool forwarded; // Master clock forwarding is enabled when true + uint32_t ticks_per_beat; // Based on the selected tempo in BPM + bool running = 0; // Specifies whether the clock is running for interprocess communication + bool paused = 0; // Specifies whethr the clock is paused + bool forwarded = 0; // Master clock forwarding is enabled when true + + uint32_t clock_tick = 0; // tick when a physical clock was received on DIGITAL 1 + uint32_t beat_tick = 0; // The tick to count from + bool tock[NR_OF_CLOCKS] = {0,0,0}; // The current tock value + int tocks_per_beat[NR_OF_CLOCKS] = {1, 1, MIDI_OUT_PPQN}; // Multiplier + int clock_ppqn = 4; // external clock multiple + bool cycle = 0; // Alternates for each tock, for display purposes + int count[NR_OF_CLOCKS] = {0,0,0}; // Multiple counter, 0 is a special case when first starting the clock + + bool boop[4]; // Manual triggers ClockManager() { SetTempoBPM(120); - SetMultiply(1); - running = 0; - paused = 0; - cycle = 0; - last_tock_tick = 0; - last_tock_check = 0; - count = 0; - tock = 0; - forwarded = 0; } public: @@ -60,9 +71,14 @@ class ClockManager { return instance; } - void SetMultiply(int8_t multiply) { - multiply = constrain(multiply, 1, 24); - tocks_per_beat = multiply; + void SetMultiply(int multiply, bool ch = 0) { + multiply = constrain(multiply, CLOCK_MIN_MULTIPLE, CLOCK_MAX_MULTIPLE); + tocks_per_beat[ch] = multiply; + } + + // adjusts the expected clock multiple for external clock pulses + void SetClockPPQN(int clkppqn) { + clock_ppqn = constrain(clkppqn, 0, 24); } /* Set ticks per tock, based on one million ticks per minute divided by beats per minute. @@ -71,39 +87,121 @@ class ClockManager { */ void SetTempoBPM(uint16_t bpm) { bpm = constrain(bpm, CLOCK_TEMPO_MIN, CLOCK_TEMPO_MAX); - ticks_per_tock = 1000000 / bpm; + ticks_per_beat = 1000000 / bpm; tempo = bpm; } - int8_t GetMultiply() {return tocks_per_beat;} + int GetMultiply(bool ch = 0) {return tocks_per_beat[ch];} + int GetClockPPQN() { return clock_ppqn; } /* Gets the current tempo. This can be used between client processes, like two different * hemispheres. */ uint16_t GetTempo() {return tempo;} - void Reset() { - last_tock_tick = OC::CORE::ticks; - count = 0; + // Resync multipliers, optionally skipping the first tock + void Reset(bool count_skip = 0) { + beat_tick = OC::CORE::ticks; + for (int ch = 0; ch < NR_OF_CLOCKS; ch++) { + if (tocks_per_beat[ch] > 0 || 0 == count_skip) count[ch] = count_skip; + } cycle = 1 - cycle; } - void Start() { - forwarded = 0; + // used to align the internal clock with incoming clock pulses + void Nudge(int diff) { + if (diff > 0) diff--; + if (diff < 0) diff++; + beat_tick += diff; + } + + // called on every tick when clock is running, before all Controllers + void SyncTrig(bool clocked) { + uint32_t now = OC::CORE::ticks; + + // Reset only when all multipliers have been met + bool reset = 1; + + // count and calculate Tocks + for (int ch = 0; ch < NR_OF_CLOCKS; ch++) { + if (tocks_per_beat[ch] == 0) { // disabled + tock[ch] = 0; continue; + } + + if (tocks_per_beat[ch] > 0) { // multiply + uint32_t next_tock_tick = beat_tick + count[ch]*ticks_per_beat / static_cast(tocks_per_beat[ch]); + tock[ch] = now >= next_tock_tick; + if (tock[ch]) ++count[ch]; // increment multiplier counter + + reset = reset && (count[ch] > tocks_per_beat[ch]); // multiplier has been exceeded + } else { // division: -1 becomes /2, -2 becomes /3, etc. + int div = 1 - tocks_per_beat[ch]; + uint32_t next_beat = beat_tick + (count[ch] ? ticks_per_beat : 0); + bool beat_exceeded = (now > next_beat); + if (beat_exceeded) { + ++count[ch]; + tock[ch] = (count[ch] % div) == 1; + } + else + tock[ch] = 0; + + // resync on every beat + reset = reset && beat_exceeded; + if (tock[ch]) count[ch] = 1; + } + + } + if (reset) Reset(1); // skip the one we're already on + + // handle syncing to physical clocks + if (clocked && clock_tick && clock_ppqn) { + + uint32_t clock_diff = now - clock_tick; + if (clock_ppqn * clock_diff > CLOCK_TICKS_MAX) clock_tick = 0; // too slow, reset clock tracking + + // if there is a previous clock tick, update tempo and sync + if (clock_tick && clock_diff) { + // update the tempo + ticks_per_beat = constrain(clock_ppqn * clock_diff, CLOCK_TICKS_MIN, CLOCK_TICKS_MAX); // time since last clock is new tempo + tempo = 1000000 / ticks_per_beat; // imprecise, for display purposes + + int ticks_per_clock = ticks_per_beat / clock_ppqn; // rounded down + + // time since last beat + int tick_offset = now - beat_tick; + + // too long ago? time til next beat + if (tick_offset > ticks_per_clock / 2) tick_offset -= ticks_per_beat; + + // within half a clock pulse of the nearest beat AND significantly large + if (abs(tick_offset) < ticks_per_clock / 2 && abs(tick_offset) > 4) + Nudge(tick_offset); // nudge the beat towards us + + } + } + // clock has been physically ticked + if (clocked) clock_tick = now; + + } + + void Start(bool p = 0) { + Reset(); running = 1; - Unpause(); + paused = p; } void Stop() { running = 0; - Unpause(); + paused = 0; } void Pause() {paused = 1;} - void Unpause() {paused = 0;} + void ToggleForwarding() { + forwarded = 1 - forwarded; + } - void ToggleForwarding() {forwarded = 1 - forwarded;} + void SetForwarding(bool f) {forwarded = f;} bool IsRunning() {return (running && !paused);} @@ -111,23 +209,31 @@ class ClockManager { bool IsForwarded() {return forwarded;} - /* Returns true if the clock should fire on this tick, based on the current tempo */ - bool Tock() { - uint32_t now = OC::CORE::ticks; - if (now != last_tock_check) { - last_tock_check = now; - if (now >= (last_tock_tick + (ticks_per_tock / static_cast(tocks_per_beat)))) { - tock = 1; - last_tock_tick = now; - if (++count >= tocks_per_beat) Reset(); - } else tock = 0; + // beep boop + void Boop(int ch = 0) { + boop[ch] = true; + } + bool Beep(int ch = 0) { + if (boop[ch]) { + boop[ch] = false; + return true; } - return tock; + return false; + } + + /* Returns true if the clock should fire on this tick, based on the current tempo and multiplier */ + bool Tock(int ch = 0) { + return tock[ch]; + } + + // Returns true if MIDI Clock should be sent on this tick + bool MIDITock() { + return Tock(MIDI_CLOCK); } - bool EndOfBeat() {return count == 0;} + bool EndOfBeat(bool ch = 0) {return count[ch] == 1;} - bool Cycle() {return cycle;} + bool Cycle(bool ch = 0) {return cycle;} }; ClockManager *ClockManager::instance = 0; diff --git a/software/o_c_REV/HemisphereApplet.h b/software/o_c_REV/HemisphereApplet.h index a8162adda..58dcc63ca 100644 --- a/software/o_c_REV/HemisphereApplet.h +++ b/software/o_c_REV/HemisphereApplet.h @@ -74,6 +74,10 @@ typedef struct PackLocation { class HemisphereApplet { public: + static uint8_t modal_edit_mode; + static void CycleEditMode() { + ++modal_edit_mode %= 3; + } virtual const char* applet_name(); // Maximum of 9 characters virtual void Start(); @@ -181,10 +185,35 @@ class HemisphereApplet { } } + // handle modal edit mode toggle or cursor advance + void CursorAction(int &cursor, int max) { + if (modal_edit_mode) { + isEditing = !isEditing; + } else { + cursor++; + cursor %= max + 1; + ResetCursor(); + } + } + void MoveCursor(int &cursor, int direction, int max) { + cursor += direction; + if (modal_edit_mode == 2) { // wrap cursor + if (cursor < 0) cursor = max; + else cursor %= max + 1; + } else { + cursor = constrain(cursor, 0, max); + } + ResetCursor(); + } + bool EditMode() { + return (isEditing || !modal_edit_mode); + } + //////////////// Offset graphics methods //////////////////////////////////////////////////////////////////////////////// - void gfxCursor(int x, int y, int w) { - if (CursorBlink()) gfxLine(x, y, x + w - 1, y); + void gfxCursor(int x, int y, int w, int h = 9) { // assumes standard text height for highlighting + if (isEditing) gfxInvert(x, y - h, w, h); + else if (CursorBlink()) gfxLine(x, y, x + w - 1, y); } void gfxPos(int x, int y) { @@ -325,23 +354,34 @@ class HemisphereApplet { */ bool Clock(int ch, bool physical = 0) { bool clocked = 0; - if (hemisphere == 0) { - if (ch == 0) clocked = OC::DigitalInputs::clocked(); - if (ch == 1) clocked = OC::DigitalInputs::clocked(); - } else if (hemisphere == 1) { - if (ch == 0) clocked = OC::DigitalInputs::clocked(); - if (ch == 1) clocked = OC::DigitalInputs::clocked(); + ClockManager *clock_m = clock_m->get(); + + if (ch == 0) { // clock triggers + if (hemisphere == LEFT_HEMISPHERE) { + if (!physical && clock_m->IsRunning() && clock_m->GetMultiply(hemisphere) != 0) + clocked = clock_m->Tock(hemisphere); + else + clocked = OC::DigitalInputs::clocked(); + } else { // right side is special + if (!physical && clock_m->IsRunning() && clock_m->GetMultiply(hemisphere) != 0) + clocked = clock_m->Tock(hemisphere); + else if (master_clock_bus) // forwarding from left + clocked = OC::DigitalInputs::clocked(); + else + clocked = OC::DigitalInputs::clocked(); + } + } else if (ch == 1) { // simple physical trig check + if (hemisphere == LEFT_HEMISPHERE) + clocked = OC::DigitalInputs::clocked(); + else + clocked = OC::DigitalInputs::clocked(); } - if (ch == 0 && !physical) { - ClockManager *clock_m = clock_m->get(); - if (clock_m->IsRunning()) clocked = clock_m->Tock(); - else if (master_clock_bus) clocked = OC::DigitalInputs::clocked(); - } + clocked = clocked || clock_m->Beep(hemisphere*2 + ch); if (clocked) { - cycle_ticks[ch] = OC::CORE::ticks - last_clock[ch]; - last_clock[ch] = OC::CORE::ticks; + cycle_ticks[ch] = OC::CORE::ticks - last_clock[ch]; + last_clock[ch] = OC::CORE::ticks; } return clocked; } @@ -376,6 +416,7 @@ class HemisphereApplet { protected: bool hemisphere; // Which hemisphere (0, 1) this applet uses + bool isEditing = false; // modal editing toggle const char* help[4]; virtual void SetHelp(); @@ -447,6 +488,7 @@ class HemisphereApplet { } bool EndOfADCLag(int ch = 0) { + if (adc_lag_countdown[ch] < 0) return false; return (--adc_lag_countdown[ch] == 0); } @@ -466,7 +508,9 @@ class HemisphereApplet { bool master_clock_bus; // Clock forwarding was on during the last ISR cycle bool applet_started; // Allow the app to maintain state during switching int last_view_tick; // Tick number of the most recent view - int help_active; + bool help_active; bool changed_cv[2]; // Has the input changed by more than 1/8 semitone since the last read? int last_cv[2]; // For change detection }; + +uint8_t HemisphereApplet::modal_edit_mode = 1; // 0=old behavior, 1=modal editing, 2=modal with wraparound