Consistent clock behavior for ext cv and midi.

This commit is contained in:
2025-06-02 21:56:17 -07:00
parent 8cf64fefca
commit 01bf09d4f5
4 changed files with 79 additions and 51 deletions

58
clock.h
View File

@ -24,7 +24,10 @@
#define MIDI_CONTINUE 0xFB #define MIDI_CONTINUE 0xFB
const int DEFAULT_TEMPO = 120; const int DEFAULT_TEMPO = 120;
static bool MIDI_ENABLED = false;
typedef void (*ExtCallback)(void);
static ExtCallback extUserCallback = nullptr;
static void serialEventNoop(uint8_t msg, uint8_t status) {}
enum Source { enum Source {
SOURCE_INTERNAL, SOURCE_INTERNAL,
@ -38,12 +41,12 @@ class Clock {
public: public:
void Init() { void Init() {
NeoSerial.begin(31250); NeoSerial.begin(31250);
NeoSerial.attachInterrupt(onSerialEvent);
// Static pin definition for pulse out. // Static pin definition for pulse out.
pinMode(PULSE_OUT_PIN, OUTPUT); pinMode(PULSE_OUT_PIN, OUTPUT);
// Initialize the clock library // Initialize the clock library
uClock.setExtIntervalBuffer(32);
uClock.init(); uClock.init();
uClock.setClockMode(uClock.INTERNAL_CLOCK); uClock.setClockMode(uClock.INTERNAL_CLOCK);
uClock.setOutputPPQN(uClock.PPQN_96); uClock.setOutputPPQN(uClock.PPQN_96);
@ -52,14 +55,15 @@ class Clock {
// MIDI events. // MIDI events.
uClock.setOnClockStart(sendMIDIStart); uClock.setOnClockStart(sendMIDIStart);
uClock.setOnClockStop(sendMIDIStop); uClock.setOnClockStop(sendMIDIStop);
uClock.setOnSync24(sendMidiClock); uClock.setOnSync24(sendMIDIClock);
uClock.setOnSync48(sendPulseOut); uClock.setOnSync48(sendPulseOut);
uClock.start(); uClock.start();
} }
// Handler for receiving clock trigger(PPQN_4 or PPQN_24). // Handle external clock tick and call user callback when receiving clock trigger (PPQN_4, PPQN_24, or MIDI).
void AttachExtHandler(void (*callback)(void)) { void AttachExtHandler(void (*callback)()) {
extUserCallback = callback;
attachInterrupt(digitalPinToInterrupt(EXT_PIN), callback, RISING); attachInterrupt(digitalPinToInterrupt(EXT_PIN), callback, RISING);
} }
@ -70,8 +74,13 @@ class Clock {
// Set the source of the clock mode. // Set the source of the clock mode.
void SetSource(Source source) { void SetSource(Source source) {
bool was_playing = !IsPaused();
uClock.stop(); uClock.stop();
MIDI_ENABLED = false; // If source is currently MIDI, disable the serial interrupt handler.
if (source_ == SOURCE_EXTERNAL_MIDI) {
NeoSerial.attachInterrupt(serialEventNoop);
}
source_ = source;
switch (source) { switch (source) {
case SOURCE_INTERNAL: case SOURCE_INTERNAL:
uClock.setClockMode(uClock.INTERNAL_CLOCK); uClock.setClockMode(uClock.INTERNAL_CLOCK);
@ -87,52 +96,64 @@ class Clock {
case SOURCE_EXTERNAL_MIDI: case SOURCE_EXTERNAL_MIDI:
uClock.setClockMode(uClock.EXTERNAL_CLOCK); uClock.setClockMode(uClock.EXTERNAL_CLOCK);
uClock.setInputPPQN(uClock.PPQN_24); uClock.setInputPPQN(uClock.PPQN_24);
MIDI_ENABLED = true; NeoSerial.attachInterrupt(onSerialEvent);
break; break;
} }
uClock.start(); if (was_playing) {
uClock.start();
}
} }
// Return true if the current selected source is externl (PPQN_4, PPQN_24, or MIDI).
bool ExternalSource() { bool ExternalSource() {
return uClock.getClockMode() == uClock.EXTERNAL_CLOCK; return uClock.getClockMode() == uClock.EXTERNAL_CLOCK;
} }
// Return true if the current selected source is the internal master clock.
bool InternalSource() { bool InternalSource() {
return uClock.getClockMode() == uClock.INTERNAL_CLOCK; return uClock.getClockMode() == uClock.INTERNAL_CLOCK;
} }
// Returns the current BPM tempo.
int Tempo() { int Tempo() {
return uClock.getTempo(); return uClock.getTempo();
} }
// Set the clock tempo to a int between 1 and 400.
void SetTempo(int tempo) { void SetTempo(int tempo) {
return uClock.setTempo(tempo); return uClock.setTempo(tempo);
} }
// Record an external clock tick received to process external/internal syncronization.
void Tick() { void Tick() {
uClock.clockMe(); uClock.clockMe();
} }
void Pause() { // Start the internal clock.
uClock.pause(); void Start() {
}
void Reset() {
uClock.start(); uClock.start();
} }
// Stop internal clock clock.
void Stop() {
uClock.stop();
}
// Returns true if the clock is not running.
bool IsPaused() { bool IsPaused() {
return uClock.clock_state == uClock.PAUSED; return uClock.clock_state == uClock.PAUSED;
} }
private: private:
Source source_ = SOURCE_INTERNAL;
static void onSerialEvent(uint8_t msg, uint8_t status) { static void onSerialEvent(uint8_t msg, uint8_t status) {
// if (!MIDI_ENABLED) { // Note: uClock start and stop will echo to MIDI.
// return;
// }
switch (msg) { switch (msg) {
case MIDI_CLOCK: case MIDI_CLOCK:
uClock.clockMe(); if (extUserCallback) {
extUserCallback();
}
break; break;
case MIDI_STOP: case MIDI_STOP:
uClock.stop(); uClock.stop();
@ -141,7 +162,6 @@ class Clock {
case MIDI_CONTINUE: case MIDI_CONTINUE:
uClock.start(); uClock.start();
break; break;
} }
} }
@ -153,7 +173,7 @@ class Clock {
NeoSerial.write(MIDI_STOP); NeoSerial.write(MIDI_STOP);
} }
static void sendMidiClock(uint32_t tick) { static void sendMIDIClock(uint32_t tick) {
NeoSerial.write(MIDI_CLOCK); NeoSerial.write(MIDI_CLOCK);
} }

View File

@ -13,7 +13,7 @@
* *
* BTN1: Play/pause the internal clock. * BTN1: Play/pause the internal clock.
* *
* BTN2: Reset all clocks. * BTN2: Stop all clocks.
* *
*/ */
@ -68,8 +68,8 @@ void setup() {
gravity.Init(); gravity.Init();
// Clock handlers. // Clock handlers.
gravity.clock.AttachExtHandler(ExtClock); gravity.clock.AttachIntHandler(HandleIntClockTick);
gravity.clock.AttachIntHandler(IntClock); gravity.clock.AttachExtHandler(HandleExtClockTick);
// Encoder rotate and press handlers. // Encoder rotate and press handlers.
gravity.encoder.AttachPressHandler(HandleEncoderPressed); gravity.encoder.AttachPressHandler(HandleEncoderPressed);
@ -95,16 +95,9 @@ void loop() {
// Firmware handlers. // Firmware handlers.
// //
void ExtClock() { void HandleIntClockTick(uint32_t tick) {
if (gravity.clock.ExternalSource()) {
gravity.clock.Tick();
app.refresh_screen = true;
}
}
void IntClock(uint32_t tick) {
for (int i = 0; i < OUTPUT_COUNT; i++) { for (int i = 0; i < OUTPUT_COUNT; i++) {
const auto& channel = app.channel[i]; auto& channel = app.channel[i];
auto& output = gravity.outputs[i]; auto& output = gravity.outputs[i];
const uint32_t mod_pulses = clock_mod_pulses[channel.clock_mod_index]; const uint32_t mod_pulses = clock_mod_pulses[channel.clock_mod_index];
@ -126,18 +119,27 @@ void IntClock(uint32_t tick) {
} }
} }
void HandlePlayPressed() { void HandleExtClockTick() {
gravity.clock.Pause(); // Ignore tick if not using external source.
if (gravity.clock.IsPaused()) { if (!gravity.clock.ExternalSource()) {
for (int i = 0; i < OUTPUT_COUNT; i++) { return;
gravity.outputs[i].Low();
}
} }
gravity.clock.Tick();
app.refresh_screen = true;
}
void HandlePlayPressed() {
gravity.clock.IsPaused()
? gravity.clock.Start()
: gravity.clock.Stop();
ResetOutputs();
app.refresh_screen = true; app.refresh_screen = true;
} }
void HandleShiftPressed() { void HandleShiftPressed() {
gravity.clock.Reset(); gravity.clock.Stop();
ResetOutputs();
app.refresh_screen = true;
} }
void HandleEncoderPressed() { void HandleEncoderPressed() {
@ -228,6 +230,12 @@ Channel& GetSelectedChannel() {
return app.channel[app.selected_channel - 1]; return app.channel[app.selected_channel - 1];
} }
void ResetOutputs() {
for (int i = 0; i < OUTPUT_COUNT; i++) {
gravity.outputs[i].Low();
}
}
// //
// UI Display functions. // UI Display functions.
// //

View File

@ -12,17 +12,17 @@
#include "gravity.h" #include "gravity.h"
void Gravity::Init() { void Gravity::Init() {
InitClock(); initClock();
InitInputs(); initInputs();
InitOutputs(); initOutputs();
InitDisplay(); initDisplay();
} }
void Gravity::InitClock() { void Gravity::initClock() {
clock.Init(); clock.Init();
} }
void Gravity::InitInputs() { void Gravity::initInputs() {
shift_button.Init(SHIFT_BTN_PIN); shift_button.Init(SHIFT_BTN_PIN);
play_button.Init(PLAY_BTN_PIN); play_button.Init(PLAY_BTN_PIN);
@ -40,7 +40,7 @@ void Gravity::InitInputs() {
PCMSK1 |= B00001000; PCMSK1 |= B00001000;
} }
void Gravity::InitOutputs() { void Gravity::initOutputs() {
// Initialize each of the outputs with it's GPIO pins and probability. // Initialize each of the outputs with it's GPIO pins and probability.
outputs[0].Init(OUT_CH1); outputs[0].Init(OUT_CH1);
outputs[1].Init(OUT_CH2); outputs[1].Init(OUT_CH2);
@ -49,7 +49,7 @@ void Gravity::InitOutputs() {
outputs[4].Init(OUT_CH5); outputs[4].Init(OUT_CH5);
outputs[5].Init(OUT_CH6); outputs[5].Init(OUT_CH6);
} }
void Gravity::InitDisplay() { void Gravity::initDisplay() {
// OLED Display configuration. // OLED Display configuration.
display.begin(); display.begin();
} }

View File

@ -37,10 +37,10 @@ class Gravity {
AnalogInput cv2; AnalogInput cv2;
private: private:
void InitClock(); void initClock();
void InitDisplay(); void initDisplay();
void InitInputs(); void initInputs();
void InitOutputs(); void initOutputs();
}; };
extern Gravity gravity; extern Gravity gravity;