Skip to content

Commit

Permalink
Improved Internal Clock
Browse files Browse the repository at this point in the history
* Adapts to external clock at configurable PPQN (0 to disable)
* Separate internal clock multipliers/dividers for each hemisphere
  (0 to disable)
* Internal Clock sends realtime MIDI Clock/Start/Stop
* Forwarding only duplicates physical DIGITAL 1 -> DIGITAL 3
* Clock state data is stored with ClockSetup, not Metronome
  • Loading branch information
djphazer committed Feb 9, 2023
1 parent 3ecda61 commit 5cec6bc
Show file tree
Hide file tree
Showing 5 changed files with 379 additions and 128 deletions.
34 changes: 24 additions & 10 deletions software/o_c_REV/APP_HEMISPHERE.ino
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
};

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<OC::DIGITAL_INPUT_1>() );

// NJM: always execute ClockSetup controller - it handles MIDI clock out
ClockSetup.Controller(LEFT_HEMISPHERE, clock_m->IsForwarded());

for (int h = 0; h < 2; h++)
{
Expand Down Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
Expand Down
179 changes: 137 additions & 42 deletions software/o_c_REV/HEM_ClockSetup.ino
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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;
}
}
};

Expand Down
16 changes: 4 additions & 12 deletions software/o_c_REV/HEM_Metronome.ino
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand All @@ -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:
Expand All @@ -77,7 +70,6 @@ protected:
}

private:
int cursor; // 0=Tempo, 1=Multiply, 2=Start/Stop
ClockManager *clock_m = clock_m->get();

void DrawInterface() {
Expand Down
Loading

0 comments on commit 5cec6bc

Please sign in to comment.