summaryrefslogtreecommitdiff
path: root/quantum
diff options
context:
space:
mode:
Diffstat (limited to 'quantum')
-rw-r--r--quantum/backlight/backlight_avr.c12
-rw-r--r--quantum/backlight/backlight_chibios.c14
-rw-r--r--quantum/config_common.h92
-rw-r--r--quantum/encoder.c24
-rw-r--r--quantum/joystick.h12
-rw-r--r--quantum/led_matrix.c2
-rw-r--r--quantum/led_matrix.h (renamed from quantum/ledmatrix.h)0
-rw-r--r--quantum/led_matrix_drivers.c2
-rw-r--r--quantum/matrix.c35
-rw-r--r--quantum/mcu_selection.mk3
-rw-r--r--quantum/process_keycode/process_auto_shift.c213
-rw-r--r--quantum/process_keycode/process_auto_shift.h11
-rw-r--r--quantum/process_keycode/process_joystick.c8
-rw-r--r--quantum/process_keycode/process_midi.c25
-rw-r--r--quantum/process_keycode/process_midi.h2
-rw-r--r--quantum/process_keycode/process_sequencer.c62
-rw-r--r--quantum/process_keycode/process_sequencer.h21
-rw-r--r--quantum/quantum.c15
-rw-r--r--quantum/quantum.h62
-rw-r--r--quantum/quantum_keycodes.h38
-rw-r--r--quantum/rgb_matrix.c32
-rw-r--r--quantum/rgb_matrix.h9
-rw-r--r--quantum/rgb_matrix_animations/alpha_mods_anim.h4
-rw-r--r--quantum/rgb_matrix_animations/breathing_anim.h2
-rw-r--r--quantum/rgb_matrix_animations/gradient_left_right_anim.h2
-rw-r--r--quantum/rgb_matrix_animations/gradient_up_down_anim.h2
-rw-r--r--quantum/rgb_matrix_animations/jellybean_raindrops_anim.h2
-rw-r--r--quantum/rgb_matrix_animations/raindrops_anim.h2
-rw-r--r--quantum/rgb_matrix_animations/solid_color_anim.h2
-rw-r--r--quantum/rgb_matrix_animations/typing_heatmap_anim.h2
-rw-r--r--quantum/rgb_matrix_runners/effect_runner_dx_dy.h2
-rw-r--r--quantum/rgb_matrix_runners/effect_runner_dx_dy_dist.h2
-rw-r--r--quantum/rgb_matrix_runners/effect_runner_i.h2
-rw-r--r--quantum/rgb_matrix_runners/effect_runner_reactive.h2
-rw-r--r--quantum/rgb_matrix_runners/effect_runner_reactive_splash.h2
-rw-r--r--quantum/rgb_matrix_runners/effect_runner_sin_cos_i.h2
-rw-r--r--quantum/rgblight.c4
-rw-r--r--quantum/sequencer/sequencer.c275
-rw-r--r--quantum/sequencer/sequencer.h122
-rw-r--r--quantum/sequencer/tests/midi_mock.c26
-rw-r--r--quantum/sequencer/tests/midi_mock.h26
-rw-r--r--quantum/sequencer/tests/rules.mk11
-rw-r--r--quantum/sequencer/tests/sequencer_tests.cpp590
-rw-r--r--quantum/sequencer/tests/testlist.mk1
-rw-r--r--quantum/split_common/matrix.c35
45 files changed, 1677 insertions, 137 deletions
diff --git a/quantum/backlight/backlight_avr.c b/quantum/backlight/backlight_avr.c
index b3e882ffe1..4d66da80ba 100644
--- a/quantum/backlight/backlight_avr.c
+++ b/quantum/backlight/backlight_avr.c
@@ -3,6 +3,11 @@
#include "backlight_driver_common.h"
#include "debug.h"
+// Maximum duty cycle limit
+#ifndef BACKLIGHT_LIMIT_VAL
+# define BACKLIGHT_LIMIT_VAL 255
+#endif
+
// This logic is a bit complex, we support 3 setups:
//
// 1. Hardware PWM when backlight is wired to a PWM pin.
@@ -240,6 +245,9 @@ static uint16_t cie_lightness(uint16_t v) {
}
}
+// rescale the supplied backlight value to be in terms of the value limit
+static uint32_t rescale_limit_val(uint32_t val) { return (val * (BACKLIGHT_LIMIT_VAL + 1)) / 256; }
+
// range for val is [0..TIMER_TOP]. PWM pin is high while the timer count is below val.
static inline void set_pwm(uint16_t val) { OCRxx = val; }
@@ -269,7 +277,7 @@ void backlight_set(uint8_t level) {
#endif
}
// Set the brightness
- set_pwm(cie_lightness(TIMER_TOP * (uint32_t)level / BACKLIGHT_LEVELS));
+ set_pwm(cie_lightness(rescale_limit_val(TIMER_TOP * (uint32_t)level / BACKLIGHT_LEVELS)));
}
void backlight_task(void) {}
@@ -375,7 +383,7 @@ ISR(TIMERx_OVF_vect)
breathing_interrupt_disable();
}
- set_pwm(cie_lightness(scale_backlight((uint16_t)pgm_read_byte(&breathing_table[index]) * 0x0101U)));
+ set_pwm(cie_lightness(rescale_limit_val(scale_backlight((uint16_t)pgm_read_byte(&breathing_table[index]) * 0x0101U))));
}
#endif // BACKLIGHT_BREATHING
diff --git a/quantum/backlight/backlight_chibios.c b/quantum/backlight/backlight_chibios.c
index 0fe812bf27..4d5a69e14e 100644
--- a/quantum/backlight/backlight_chibios.c
+++ b/quantum/backlight/backlight_chibios.c
@@ -3,6 +3,11 @@
#include <hal.h>
#include "debug.h"
+// Maximum duty cycle limit
+#ifndef BACKLIGHT_LIMIT_VAL
+# define BACKLIGHT_LIMIT_VAL 255
+#endif
+
// GPIOV2 && GPIOV3
#ifndef BACKLIGHT_PAL_MODE
# define BACKLIGHT_PAL_MODE 2
@@ -58,6 +63,11 @@ static uint16_t cie_lightness(uint16_t v) {
}
}
+static uint32_t rescale_limit_val(uint32_t val) {
+ // rescale the supplied backlight value to be in terms of the value limit
+ return (val * (BACKLIGHT_LIMIT_VAL + 1)) / 256;
+}
+
void backlight_init_ports(void) {
#ifdef USE_GPIOV1
palSetPadMode(PAL_PORT(BACKLIGHT_PIN), PAL_PAD(BACKLIGHT_PIN), PAL_MODE_STM32_ALTERNATE_PUSHPULL);
@@ -85,7 +95,7 @@ void backlight_set(uint8_t level) {
pwmDisableChannel(&BACKLIGHT_PWM_DRIVER, BACKLIGHT_PWM_CHANNEL - 1);
} else {
// Turn backlight on
- uint32_t duty = (uint32_t)(cie_lightness(0xFFFF * (uint32_t)level / BACKLIGHT_LEVELS));
+ uint32_t duty = (uint32_t)(cie_lightness(rescale_limit_val(0xFFFF * (uint32_t)level / BACKLIGHT_LEVELS)));
pwmEnableChannel(&BACKLIGHT_PWM_DRIVER, BACKLIGHT_PWM_CHANNEL - 1, PWM_FRACTION_TO_WIDTH(&BACKLIGHT_PWM_DRIVER, 0xFFFF, duty));
}
}
@@ -129,7 +139,7 @@ void breathing_callback(PWMDriver *pwmp) {
static uint16_t breathing_counter = 0;
breathing_counter = (breathing_counter + 1) % (breathing_period * 256);
uint8_t index = breathing_counter / interval % BREATHING_STEPS;
- uint32_t duty = cie_lightness(scale_backlight(breathing_table[index] * 256));
+ uint32_t duty = cie_lightness(rescale_limit_val(scale_backlight(breathing_table[index] * 256)));
chSysLockFromISR();
pwmEnableChannelI(pwmp, BACKLIGHT_PWM_CHANNEL - 1, PWM_FRACTION_TO_WIDTH(&BACKLIGHT_PWM_DRIVER, 0xFFFF, duty));
diff --git a/quantum/config_common.h b/quantum/config_common.h
index c6a96e673c..53377f2b61 100644
--- a/quantum/config_common.h
+++ b/quantum/config_common.h
@@ -40,7 +40,7 @@
# define PIND_ADDRESS 0x9
# define PINE_ADDRESS 0xC
# define PINF_ADDRESS 0xF
-# elif defined(__AVR_ATmega32U2__) || defined(__AVR_ATmega16U2__)
+# elif defined(__AVR_ATmega32U2__) || defined(__AVR_ATmega16U2__) || defined(__AVR_ATmega328P__) || defined(__AVR_ATmega328__)
# define ADDRESS_BASE 0x00
# define PINB_ADDRESS 0x3
# define PINC_ADDRESS 0x6
@@ -59,11 +59,6 @@
# define PINC_ADDRESS 0x3
# define PINB_ADDRESS 0x6
# define PINA_ADDRESS 0x9
-# elif defined(__AVR_ATmega328P__) || defined(__AVR_ATmega328__)
-# define ADDRESS_BASE 0x00
-# define PINB_ADDRESS 0x3
-# define PINC_ADDRESS 0x6
-# define PIND_ADDRESS 0x9
# elif defined(__AVR_ATtiny85__)
# define ADDRESS_BASE 0x10
# define PINB_ADDRESS 0x6
@@ -285,6 +280,91 @@
# define F13 PAL_LINE(GPIOF, 13)
# define F14 PAL_LINE(GPIOF, 14)
# define F15 PAL_LINE(GPIOF, 15)
+# define G0 PAL_LINE(GPIOG, 0)
+# define G1 PAL_LINE(GPIOG, 1)
+# define G2 PAL_LINE(GPIOG, 2)
+# define G3 PAL_LINE(GPIOG, 3)
+# define G4 PAL_LINE(GPIOG, 4)
+# define G5 PAL_LINE(GPIOG, 5)
+# define G6 PAL_LINE(GPIOG, 6)
+# define G7 PAL_LINE(GPIOG, 7)
+# define G8 PAL_LINE(GPIOG, 8)
+# define G9 PAL_LINE(GPIOG, 9)
+# define G10 PAL_LINE(GPIOG, 10)
+# define G11 PAL_LINE(GPIOG, 11)
+# define G12 PAL_LINE(GPIOG, 12)
+# define G13 PAL_LINE(GPIOG, 13)
+# define G14 PAL_LINE(GPIOG, 14)
+# define G15 PAL_LINE(GPIOG, 15)
+# define H0 PAL_LINE(GPIOH, 0)
+# define H1 PAL_LINE(GPIOH, 1)
+# define H2 PAL_LINE(GPIOH, 2)
+# define H3 PAL_LINE(GPIOH, 3)
+# define H4 PAL_LINE(GPIOH, 4)
+# define H5 PAL_LINE(GPIOH, 5)
+# define H6 PAL_LINE(GPIOH, 6)
+# define H7 PAL_LINE(GPIOH, 7)
+# define H8 PAL_LINE(GPIOH, 8)
+# define H9 PAL_LINE(GPIOH, 9)
+# define H10 PAL_LINE(GPIOH, 10)
+# define H11 PAL_LINE(GPIOH, 11)
+# define H12 PAL_LINE(GPIOH, 12)
+# define H13 PAL_LINE(GPIOH, 13)
+# define H14 PAL_LINE(GPIOH, 14)
+# define H15 PAL_LINE(GPIOH, 15)
+# define I0 PAL_LINE(GPIOI, 0)
+# define I1 PAL_LINE(GPIOI, 1)
+# define I2 PAL_LINE(GPIOI, 2)
+# define I3 PAL_LINE(GPIOI, 3)
+# define I4 PAL_LINE(GPIOI, 4)
+# define I5 PAL_LINE(GPIOI, 5)
+# define I6 PAL_LINE(GPIOI, 6)
+# define I7 PAL_LINE(GPIOI, 7)
+# define I8 PAL_LINE(GPIOI, 8)
+# define I9 PAL_LINE(GPIOI, 9)
+# define I10 PAL_LINE(GPIOI, 10)
+# define I11 PAL_LINE(GPIOI, 11)
+# define I12 PAL_LINE(GPIOI, 12)
+# define I13 PAL_LINE(GPIOI, 13)
+# define I14 PAL_LINE(GPIOI, 14)
+# define I15 PAL_LINE(GPIOI, 15)
+# define J0 PAL_LINE(GPIOJ, 0)
+# define J1 PAL_LINE(GPIOJ, 1)
+# define J2 PAL_LINE(GPIOJ, 2)
+# define J3 PAL_LINE(GPIOJ, 3)
+# define J4 PAL_LINE(GPIOJ, 4)
+# define J5 PAL_LINE(GPIOJ, 5)
+# define J6 PAL_LINE(GPIOJ, 6)
+# define J7 PAL_LINE(GPIOJ, 7)
+# define J8 PAL_LINE(GPIOJ, 8)
+# define J9 PAL_LINE(GPIOJ, 9)
+# define J10 PAL_LINE(GPIOJ, 10)
+# define J11 PAL_LINE(GPIOJ, 11)
+# define J12 PAL_LINE(GPIOJ, 12)
+# define J13 PAL_LINE(GPIOJ, 13)
+# define J14 PAL_LINE(GPIOJ, 14)
+# define J15 PAL_LINE(GPIOJ, 15)
+// Keyboards can `#define KEYBOARD_REQUIRES_GPIOK` if they need to access GPIO-K pins. These conflict with a whole
+// bunch of layout definitions, so it's intentionally left out unless absolutely required -- in that case, the
+// keyboard designer should use a different symbol when defining their layout macros.
+# ifdef KEYBOARD_REQUIRES_GPIOK
+# define K0 PAL_LINE(GPIOK, 0)
+# define K1 PAL_LINE(GPIOK, 1)
+# define K2 PAL_LINE(GPIOK, 2)
+# define K3 PAL_LINE(GPIOK, 3)
+# define K4 PAL_LINE(GPIOK, 4)
+# define K5 PAL_LINE(GPIOK, 5)
+# define K6 PAL_LINE(GPIOK, 6)
+# define K7 PAL_LINE(GPIOK, 7)
+# define K8 PAL_LINE(GPIOK, 8)
+# define K9 PAL_LINE(GPIOK, 9)
+# define K10 PAL_LINE(GPIOK, 10)
+# define K11 PAL_LINE(GPIOK, 11)
+# define K12 PAL_LINE(GPIOK, 12)
+# define K13 PAL_LINE(GPIOK, 13)
+# define K14 PAL_LINE(GPIOK, 14)
+# define K15 PAL_LINE(GPIOK, 15)
+# endif
# endif
#endif
diff --git a/quantum/encoder.c b/quantum/encoder.c
index 81ec1bb376..7ca31afedc 100644
--- a/quantum/encoder.c
+++ b/quantum/encoder.c
@@ -23,7 +23,7 @@
// for memcpy
#include <string.h>
-#ifndef ENCODER_RESOLUTION
+#if !defined(ENCODER_RESOLUTIONS) && !defined(ENCODER_RESOLUTION)
# define ENCODER_RESOLUTION 4
#endif
@@ -34,6 +34,9 @@
#define NUMBER_OF_ENCODERS (sizeof(encoders_pad_a) / sizeof(pin_t))
static pin_t encoders_pad_a[] = ENCODERS_PAD_A;
static pin_t encoders_pad_b[] = ENCODERS_PAD_B;
+#ifdef ENCODER_RESOLUTIONS
+static uint8_t encoder_resolutions[] = ENCODER_RESOLUTIONS;
+#endif
#ifndef ENCODER_DIRECTION_FLIP
# define ENCODER_CLOCKWISE true
@@ -65,9 +68,15 @@ void encoder_init(void) {
if (!isLeftHand) {
const pin_t encoders_pad_a_right[] = ENCODERS_PAD_A_RIGHT;
const pin_t encoders_pad_b_right[] = ENCODERS_PAD_B_RIGHT;
+# if defined(ENCODER_RESOLUTIONS_RIGHT)
+ const uint8_t encoder_resolutions_right[] = ENCODER_RESOLUTIONS_RIGHT;
+# endif
for (uint8_t i = 0; i < NUMBER_OF_ENCODERS; i++) {
encoders_pad_a[i] = encoders_pad_a_right[i];
encoders_pad_b[i] = encoders_pad_b_right[i];
+# if defined(ENCODER_RESOLUTIONS_RIGHT)
+ encoder_resolutions[i] = encoder_resolutions_right[i];
+# endif
}
}
#endif
@@ -87,19 +96,26 @@ void encoder_init(void) {
static void encoder_update(int8_t index, uint8_t state) {
uint8_t i = index;
+
+#ifdef ENCODER_RESOLUTIONS
+ int8_t resolution = encoder_resolutions[i];
+#else
+ int8_t resolution = ENCODER_RESOLUTION;
+#endif
+
#ifdef SPLIT_KEYBOARD
index += thisHand;
#endif
encoder_pulses[i] += encoder_LUT[state & 0xF];
- if (encoder_pulses[i] >= ENCODER_RESOLUTION) {
+ if (encoder_pulses[i] >= resolution) {
encoder_value[index]++;
encoder_update_kb(index, ENCODER_COUNTER_CLOCKWISE);
}
- if (encoder_pulses[i] <= -ENCODER_RESOLUTION) { // direction is arbitrary here, but this clockwise
+ if (encoder_pulses[i] <= -resolution) { // direction is arbitrary here, but this clockwise
encoder_value[index]--;
encoder_update_kb(index, ENCODER_CLOCKWISE);
}
- encoder_pulses[i] %= ENCODER_RESOLUTION;
+ encoder_pulses[i] %= resolution;
}
void encoder_read(void) {
diff --git a/quantum/joystick.h b/quantum/joystick.h
index a95472b9fd..87dbc24aff 100644
--- a/quantum/joystick.h
+++ b/quantum/joystick.h
@@ -1,5 +1,9 @@
#pragma once
+#include "quantum.h"
+
+#include <stdint.h>
+
#ifndef JOYSTICK_BUTTON_COUNT
# define JOYSTICK_BUTTON_COUNT 8
#endif
@@ -8,9 +12,13 @@
# define JOYSTICK_AXES_COUNT 4
#endif
-#include "quantum.h"
+#ifndef JOYSTICK_AXES_RESOLUTION
+# define JOYSTICK_AXES_RESOLUTION 8
+#elif JOYSTICK_AXES_RESOLUTION < 8 || JOYSTICK_AXES_RESOLUTION > 16
+# error JOYSTICK_AXES_RESOLUTION must be between 8 and 16
+#endif
-#include <stdint.h>
+#define JOYSTICK_RESOLUTION ((1L << (JOYSTICK_AXES_RESOLUTION - 1)) - 1)
// configure on input_pin of the joystick_axes array entry to JS_VIRTUAL_AXIS
// to prevent it from being read from the ADC. This allows outputing forged axis value.
diff --git a/quantum/led_matrix.c b/quantum/led_matrix.c
index 5c24c797a9..eb523990a6 100644
--- a/quantum/led_matrix.c
+++ b/quantum/led_matrix.c
@@ -20,7 +20,7 @@
#include <stdint.h>
#include <stdbool.h>
#include "quantum.h"
-#include "ledmatrix.h"
+#include "led_matrix.h"
#include "progmem.h"
#include "config.h"
#include "eeprom.h"
diff --git a/quantum/ledmatrix.h b/quantum/led_matrix.h
index 5867ba9876..5867ba9876 100644
--- a/quantum/ledmatrix.h
+++ b/quantum/led_matrix.h
diff --git a/quantum/led_matrix_drivers.c b/quantum/led_matrix_drivers.c
index 6877bf4c6b..9decaa33c2 100644
--- a/quantum/led_matrix_drivers.c
+++ b/quantum/led_matrix_drivers.c
@@ -18,7 +18,7 @@
#include <stdint.h>
#include <stdbool.h>
#include "quantum.h"
-#include "ledmatrix.h"
+#include "led_matrix.h"
/* Each driver needs to define a struct:
*
diff --git a/quantum/matrix.c b/quantum/matrix.c
index c68c56cac2..cab0d2ddca 100644
--- a/quantum/matrix.c
+++ b/quantum/matrix.c
@@ -32,6 +32,19 @@ static const pin_t col_pins[MATRIX_COLS] = MATRIX_COL_PINS;
extern matrix_row_t raw_matrix[MATRIX_ROWS]; // raw values
extern matrix_row_t matrix[MATRIX_ROWS]; // debounced values
+static inline void setPinOutput_writeLow(pin_t pin) {
+ ATOMIC_BLOCK_FORCEON {
+ setPinOutput(pin);
+ writePinLow(pin);
+ }
+}
+
+static inline void setPinInputHigh_atomic(pin_t pin) {
+ ATOMIC_BLOCK_FORCEON {
+ setPinInputHigh(pin);
+ }
+}
+
// matrix code
#ifdef DIRECT_PINS
@@ -70,22 +83,23 @@ static bool read_cols_on_row(matrix_row_t current_matrix[], uint8_t current_row)
# if (DIODE_DIRECTION == COL2ROW)
static void select_row(uint8_t row) {
- setPinOutput(row_pins[row]);
- writePinLow(row_pins[row]);
+ setPinOutput_writeLow(row_pins[row]);
}
-static void unselect_row(uint8_t row) { setPinInputHigh(row_pins[row]); }
+static void unselect_row(uint8_t row) {
+ setPinInputHigh_atomic(row_pins[row]);
+}
static void unselect_rows(void) {
for (uint8_t x = 0; x < MATRIX_ROWS; x++) {
- setPinInputHigh(row_pins[x]);
+ setPinInputHigh_atomic(row_pins[x]);
}
}
static void init_pins(void) {
unselect_rows();
for (uint8_t x = 0; x < MATRIX_COLS; x++) {
- setPinInputHigh(col_pins[x]);
+ setPinInputHigh_atomic(col_pins[x]);
}
}
@@ -120,22 +134,23 @@ static bool read_cols_on_row(matrix_row_t current_matrix[], uint8_t current_row)
# elif (DIODE_DIRECTION == ROW2COL)
static void select_col(uint8_t col) {
- setPinOutput(col_pins[col]);
- writePinLow(col_pins[col]);
+ setPinOutput_writeLow(col_pins[col]);
}
-static void unselect_col(uint8_t col) { setPinInputHigh(col_pins[col]); }
+static void unselect_col(uint8_t col) {
+ setPinInputHigh_atomic(col_pins[col]);
+}
static void unselect_cols(void) {
for (uint8_t x = 0; x < MATRIX_COLS; x++) {
- setPinInputHigh(col_pins[x]);
+ setPinInputHigh_atomic(col_pins[x]);
}
}
static void init_pins(void) {
unselect_cols();
for (uint8_t x = 0; x < MATRIX_ROWS; x++) {
- setPinInputHigh(row_pins[x]);
+ setPinInputHigh_atomic(row_pins[x]);
}
}
diff --git a/quantum/mcu_selection.mk b/quantum/mcu_selection.mk
index 6fb5f88b05..ea88d9ab9e 100644
--- a/quantum/mcu_selection.mk
+++ b/quantum/mcu_selection.mk
@@ -318,6 +318,9 @@ ifneq (,$(filter $(MCU),atmega16u2 atmega32u2 atmega16u4 atmega32u4 at90usb646 a
ifeq (,$(filter $(NO_INTERRUPT_CONTROL_ENDPOINT),yes))
OPT_DEFS += -DINTERRUPT_CONTROL_ENDPOINT
endif
+ ifneq (,$(filter $(MCU),atmega16u2 atmega32u2))
+ NO_I2C = yes
+ endif
endif
ifneq (,$(filter $(MCU),atmega32a))
diff --git a/quantum/process_keycode/process_auto_shift.c b/quantum/process_keycode/process_auto_shift.c
index 76530959c9..41e60496b9 100644
--- a/quantum/process_keycode/process_auto_shift.c
+++ b/quantum/process_keycode/process_auto_shift.c
@@ -16,49 +16,149 @@
#ifdef AUTO_SHIFT_ENABLE
+# include <stdbool.h>
# include <stdio.h>
# include "process_auto_shift.h"
-static bool autoshift_enabled = true;
-static uint16_t autoshift_time = 0;
+static uint16_t autoshift_time = 0;
static uint16_t autoshift_timeout = AUTO_SHIFT_TIMEOUT;
static uint16_t autoshift_lastkey = KC_NO;
+static struct {
+ // Whether autoshift is enabled.
+ bool enabled : 1;
+ // Whether the last auto-shifted key was released after the timeout. This
+ // is used to replicate the last key for a tap-then-hold.
+ bool lastshifted : 1;
+ // Whether an auto-shiftable key has been pressed but not processed.
+ bool in_progress : 1;
+ // Whether the auto-shifted keypress has been registered.
+ bool holding_shift : 1;
+} autoshift_flags = {true, false, false, false};
+
+/** \brief Record the press of an autoshiftable key
+ *
+ * \return Whether the record should be further processed.
+ */
+static bool autoshift_press(uint16_t keycode, uint16_t now, keyrecord_t *record) {
+ if (!autoshift_flags.enabled) {
+ return true;
+ }
-void autoshift_flush(void) {
- if (autoshift_lastkey != KC_NO) {
- uint16_t elapsed = timer_elapsed(autoshift_time);
+# ifndef AUTO_SHIFT_MODIFIERS
+ if (get_mods() & (~MOD_BIT(KC_LSFT))) {
+ return true;
+ }
+# endif
+# ifdef AUTO_SHIFT_REPEAT
+ const uint16_t elapsed = TIMER_DIFF_16(now, autoshift_time);
+# ifndef AUTO_SHIFT_NO_AUTO_REPEAT
+ if (!autoshift_flags.lastshifted) {
+# endif
+ if (elapsed < TAPPING_TERM && keycode == autoshift_lastkey) {
+ // Allow a tap-then-hold for keyrepeat.
+ if (!autoshift_flags.lastshifted) {
+ register_code(autoshift_lastkey);
+ } else {
+ // Simulate pressing the shift key.
+ add_weak_mods(MOD_BIT(KC_LSFT));
+ register_code(autoshift_lastkey);
+ }
+ return false;
+ }
+# ifndef AUTO_SHIFT_NO_AUTO_REPEAT
+ }
+# endif
+# endif
+
+ // Record the keycode so we can simulate it later.
+ autoshift_lastkey = keycode;
+ autoshift_time = now;
+ autoshift_flags.in_progress = true;
- if (elapsed > autoshift_timeout) {
- tap_code16(LSFT(autoshift_lastkey));
+# if !defined(NO_ACTION_ONESHOT) && !defined(NO_ACTION_TAPPING)
+ clear_oneshot_layer_state(ONESHOT_OTHER_KEY_PRESSED);
+# endif
+ return false;
+}
+
+/** \brief Registers an autoshiftable key under the right conditions
+ *
+ * If the autoshift delay has elapsed, register a shift and the key.
+ *
+ * If the autoshift key is released before the delay has elapsed, register the
+ * key without a shift.
+ */
+static void autoshift_end(uint16_t keycode, uint16_t now, bool matrix_trigger) {
+ // Called on key down with KC_NO, auto-shifted key up, and timeout.
+ if (autoshift_flags.in_progress) {
+ // Process the auto-shiftable key.
+ autoshift_flags.in_progress = false;
+
+ // Time since the initial press was recorded.
+ const uint16_t elapsed = TIMER_DIFF_16(now, autoshift_time);
+ if (elapsed < autoshift_timeout) {
+ register_code(autoshift_lastkey);
+ autoshift_flags.lastshifted = false;
} else {
- tap_code(autoshift_lastkey);
+ // Simulate pressing the shift key.
+ add_weak_mods(MOD_BIT(KC_LSFT));
+ register_code(autoshift_lastkey);
+ autoshift_flags.lastshifted = true;
+# if defined(AUTO_SHIFT_REPEAT) && !defined(AUTO_SHIFT_NO_AUTO_REPEAT)
+ if (matrix_trigger) {
+ // Prevents release.
+ return;
+ }
+# endif
}
- autoshift_time = 0;
- autoshift_lastkey = KC_NO;
+# if TAP_CODE_DELAY > 0
+ wait_ms(TAP_CODE_DELAY);
+# endif
+ unregister_code(autoshift_lastkey);
+ del_weak_mods(MOD_BIT(KC_LSFT));
+ } else {
+ // Release after keyrepeat.
+ unregister_code(keycode);
+ if (keycode == autoshift_lastkey) {
+ // This will only fire when the key was the last auto-shiftable
+ // pressed. That prevents aaaaBBBB then releasing a from unshifting
+ // later Bs (if B wasn't auto-shiftable).
+ del_weak_mods(MOD_BIT(KC_LSFT));
+ }
}
+ send_keyboard_report(); // del_weak_mods doesn't send one.
+ // Roll the autoshift_time forward for detecting tap-and-hold.
+ autoshift_time = now;
}
-
-void autoshift_on(uint16_t keycode) {
- autoshift_time = timer_read();
- autoshift_lastkey = keycode;
+/** \brief Simulates auto-shifted key releases when timeout is hit
+ *
+ * Can be called from \c matrix_scan_user so that auto-shifted keys are sent
+ * immediately after the timeout has expired, rather than waiting for the key
+ * to be released.
+ */
+void autoshift_matrix_scan(void) {
+ if (autoshift_flags.in_progress) {
+ const uint16_t now = timer_read();
+ const uint16_t elapsed = TIMER_DIFF_16(now, autoshift_time);
+ if (elapsed >= autoshift_timeout) {
+ autoshift_end(autoshift_lastkey, now, true);
+ }
+ }
}
void autoshift_toggle(void) {
- if (autoshift_enabled) {
- autoshift_enabled = false;
- autoshift_flush();
- } else {
- autoshift_enabled = true;
- }
+ autoshift_flags.enabled = !autoshift_flags.enabled;
+ del_weak_mods(MOD_BIT(KC_LSFT));
}
-void autoshift_enable(void) { autoshift_enabled = true; }
+void autoshift_enable(void) { autoshift_flags.enabled = true; }
+
void autoshift_disable(void) {
- autoshift_enabled = false;
- autoshift_flush();
+ autoshift_flags.enabled = false;
+ del_weak_mods(MOD_BIT(KC_LSFT));
}
# ifndef AUTO_SHIFT_NO_SETUP
@@ -71,24 +171,30 @@ void autoshift_timer_report(void) {
}
# endif
-bool get_autoshift_state(void) { return autoshift_enabled; }
+bool get_autoshift_state(void) { return autoshift_flags.enabled; }
-uint16_t get_autoshift_timeout(void) {
- return autoshift_timeout;
-}
+uint16_t get_autoshift_timeout(void) { return autoshift_timeout; }
-void set_autoshift_timeout(uint16_t timeout) {
- autoshift_timeout = timeout;
-}
+void set_autoshift_timeout(uint16_t timeout) { autoshift_timeout = timeout; }
bool process_auto_shift(uint16_t keycode, keyrecord_t *record) {
+ // Note that record->event.time isn't reliable, see:
+ // https://github.com/qmk/qmk_firmware/pull/9826#issuecomment-733559550
+ const uint16_t now = timer_read();
if (record->event.pressed) {
+ if (autoshift_flags.in_progress) {
+ // Evaluate previous key if there is one. Doing this elsewhere is
+ // more complicated and easier to break.
+ autoshift_end(KC_NO, now, false);
+ }
+ // For pressing another key while keyrepeating shifted autoshift.
+ del_weak_mods(MOD_BIT(KC_LSFT));
+
switch (keycode) {
case KC_ASTG:
autoshift_toggle();
return true;
-
case KC_ASON:
autoshift_enable();
return true;
@@ -108,43 +214,30 @@ bool process_auto_shift(uint16_t keycode, keyrecord_t *record) {
autoshift_timer_report();
return true;
# endif
+ }
+ }
+
+ switch (keycode) {
# ifndef NO_AUTO_SHIFT_ALPHA
- case KC_A ... KC_Z:
+ case KC_A ... KC_Z:
# endif
# ifndef NO_AUTO_SHIFT_NUMERIC
- case KC_1 ... KC_0:
+ case KC_1 ... KC_0:
# endif
# ifndef NO_AUTO_SHIFT_SPECIAL
-# ifndef NO_AUTO_SHIFT_TAB
- case KC_TAB:
-# endif
- case KC_MINUS ... KC_SLASH:
- case KC_NONUS_BSLASH:
-# endif
- autoshift_flush();
- if (!autoshift_enabled) return true;
-
-# ifndef AUTO_SHIFT_MODIFIERS
- if (get_mods()) {
- return true;
- }
-# endif
- autoshift_on(keycode);
-
- // We need some extra handling here for OSL edge cases
-# if !defined(NO_ACTION_ONESHOT) && !defined(NO_ACTION_TAPPING)
- clear_oneshot_layer_state(ONESHOT_OTHER_KEY_PRESSED);
+# ifndef NO_AUTO_SHIFT_TAB
+ case KC_TAB:
+# endif
+ case KC_MINUS ... KC_SLASH:
+ case KC_NONUS_BSLASH:
# endif
+ if (record->event.pressed) {
+ return autoshift_press(keycode, now, record);
+ } else {
+ autoshift_end(keycode, now, false);
return false;
-
- default:
- autoshift_flush();
- return true;
- }
- } else {
- autoshift_flush();
+ }
}
-
return true;
}
diff --git a/quantum/process_keycode/process_auto_shift.h b/quantum/process_keycode/process_auto_shift.h
index b909d28eef..5b2718f11c 100644
--- a/quantum/process_keycode/process_auto_shift.h
+++ b/quantum/process_keycode/process_auto_shift.h
@@ -24,9 +24,10 @@
bool process_auto_shift(uint16_t keycode, keyrecord_t *record);
-void autoshift_enable(void);
-void autoshift_disable(void);
-void autoshift_toggle(void);
-bool get_autoshift_state(void);
+void autoshift_enable(void);
+void autoshift_disable(void);
+void autoshift_toggle(void);
+bool get_autoshift_state(void);
uint16_t get_autoshift_timeout(void);
-void set_autoshift_timeout(uint16_t timeout);
+void set_autoshift_timeout(uint16_t timeout);
+void autoshift_matrix_scan(void);
diff --git a/quantum/process_keycode/process_joystick.c b/quantum/process_keycode/process_joystick.c
index 5778a7434c..3ffaf42bf8 100644
--- a/quantum/process_keycode/process_joystick.c
+++ b/quantum/process_keycode/process_joystick.c
@@ -129,17 +129,17 @@ bool process_joystick_analogread_quantum() {
// test the converted value against the lower range
int32_t ref = joystick_axes[axis_index].mid_digit;
int32_t range = joystick_axes[axis_index].min_digit;
- int32_t ranged_val = ((axis_val - ref) * -127) / (range - ref);
+ int32_t ranged_val = ((axis_val - ref) * -JOYSTICK_RESOLUTION) / (range - ref);
if (ranged_val > 0) {
// the value is in the higher range
range = joystick_axes[axis_index].max_digit;
- ranged_val = ((axis_val - ref) * 127) / (range - ref);
+ ranged_val = ((axis_val - ref) * JOYSTICK_RESOLUTION) / (range - ref);
}
// clamp the result in the valid range
- ranged_val = ranged_val < -127 ? -127 : ranged_val;
- ranged_val = ranged_val > 127 ? 127 : ranged_val;
+ ranged_val = ranged_val < -JOYSTICK_RESOLUTION ? -JOYSTICK_RESOLUTION : ranged_val;
+ ranged_val = ranged_val > JOYSTICK_RESOLUTION ? JOYSTICK_RESOLUTION : ranged_val;
if (ranged_val != joystick_status.axes[axis_index]) {
joystick_status.axes[axis_index] = ranged_val;
diff --git a/quantum/process_keycode/process_midi.c b/quantum/process_keycode/process_midi.c
index e525770144..8e2fb955e7 100644
--- a/quantum/process_keycode/process_midi.c
+++ b/quantum/process_keycode/process_midi.c
@@ -41,12 +41,12 @@ static int8_t midi_modulation_step;
static uint16_t midi_modulation_timer;
midi_config_t midi_config;
-inline uint8_t compute_velocity(uint8_t setting) { return (setting + 1) * (128 / (MIDI_VELOCITY_MAX - MIDI_VELOCITY_MIN + 1)); }
+inline uint8_t compute_velocity(uint8_t setting) { return setting * (128 / (MIDI_VELOCITY_MAX - MIDI_VELOCITY_MIN)); }
void midi_init(void) {
midi_config.octave = MI_OCT_2 - MIDI_OCTAVE_MIN;
midi_config.transpose = 0;
- midi_config.velocity = (MIDI_VELOCITY_MAX - MIDI_VELOCITY_MIN);
+ midi_config.velocity = 127;
midi_config.channel = 0;
midi_config.modulation_interval = 8;
@@ -66,7 +66,7 @@ bool process_midi(uint16_t keycode, keyrecord_t *record) {
case MIDI_TONE_MIN ... MIDI_TONE_MAX: {
uint8_t channel = midi_config.channel;
uint8_t tone = keycode - MIDI_TONE_MIN;
- uint8_t velocity = compute_velocity(midi_config.velocity);
+ uint8_t velocity = midi_config.velocity;
if (record->event.pressed) {
if (tone_status[tone] == MIDI_INVALID_NOTE) {
uint8_t note = midi_compute_note(keycode);
@@ -124,19 +124,30 @@ bool process_midi(uint16_t keycode, keyrecord_t *record) {
return false;
case MIDI_VELOCITY_MIN ... MIDI_VELOCITY_MAX:
if (record->event.pressed) {
- midi_config.velocity = keycode - MIDI_VELOCITY_MIN;
+ midi_config.velocity = compute_velocity(keycode - MIDI_VELOCITY_MIN);
dprintf("midi velocity %d\n", midi_config.velocity);
}
return false;
case MI_VELD:
if (record->event.pressed && midi_config.velocity > 0) {
- midi_config.velocity--;
+ if (midi_config.velocity == 127) {
+ midi_config.velocity -= 10;
+ } else if (midi_config.velocity > 12) {
+ midi_config.velocity -= 13;
+ } else {
+ midi_config.velocity = 0;
+ }
+
dprintf("midi velocity %d\n", midi_config.velocity);
}
return false;
case MI_VELU:
- if (record->event.pressed) {
- midi_config.velocity++;
+ if (record->event.pressed && midi_config.velocity < 127) {
+ if (midi_config.velocity < 115) {
+ midi_config.velocity += 13;
+ } else {
+ midi_config.velocity = 127;
+ }
dprintf("midi velocity %d\n", midi_config.velocity);
}
return false;
diff --git a/quantum/process_keycode/process_midi.h b/quantum/process_keycode/process_midi.h
index 0007b3ed25..ef5661dd4d 100644
--- a/quantum/process_keycode/process_midi.h
+++ b/quantum/process_keycode/process_midi.h
@@ -35,7 +35,7 @@ typedef union {
struct {
uint8_t octave : 4;
int8_t transpose : 4;
- uint8_t velocity : 4;
+ uint8_t velocity : 7;
uint8_t channel : 4;
uint8_t modulation_interval : 4;
};
diff --git a/quantum/process_keycode/process_sequencer.c b/quantum/process_keycode/process_sequencer.c
new file mode 100644
index 0000000000..334b4c0092
--- /dev/null
+++ b/quantum/process_keycode/process_sequencer.c
@@ -0,0 +1,62 @@
+/* Copyright 2020 Rodolphe Belouin
+ *
+ * This program 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "process_sequencer.h"
+
+bool process_sequencer(uint16_t keycode, keyrecord_t *record) {
+ if (record->event.pressed) {
+ switch (keycode) {
+ case SQ_ON:
+ sequencer_on();
+ return false;
+ case SQ_OFF:
+ sequencer_off();
+ return false;
+ case SQ_TOG:
+ sequencer_toggle();
+ return false;
+ case SQ_TMPD:
+ sequencer_decrease_tempo();
+ return false;
+ case SQ_TMPU:
+ sequencer_increase_tempo();
+ return false;
+ case SEQUENCER_RESOLUTION_MIN ... SEQUENCER_RESOLUTION_MAX:
+ sequencer_set_resolution(keycode - SEQUENCER_RESOLUTION_MIN);
+ return false;
+ case SQ_RESD:
+ sequencer_decrease_resolution();
+ return false;
+ case SQ_RESU:
+ sequencer_increase_resolution();
+ return false;
+ case SQ_SALL:
+ sequencer_set_all_steps_on();
+ return false;
+ case SQ_SCLR:
+ sequencer_set_all_steps_off();
+ return false;
+ case SEQUENCER_STEP_MIN ... SEQUENCER_STEP_MAX:
+ sequencer_toggle_step(keycode - SEQUENCER_STEP_MIN);
+ return false;
+ case SEQUENCER_TRACK_MIN ... SEQUENCER_TRACK_MAX:
+ sequencer_toggle_single_active_track(keycode - SEQUENCER_TRACK_MIN);
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/quantum/process_keycode/process_sequencer.h b/quantum/process_keycode/process_sequencer.h
new file mode 100644
index 0000000000..2b85f24299
--- /dev/null
+++ b/quantum/process_keycode/process_sequencer.h
@@ -0,0 +1,21 @@
+/* Copyright 2020 Rodolphe Belouin
+ *
+ * This program 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "quantum.h"
+
+bool process_sequencer(uint16_t keycode, keyrecord_t *record);
diff --git a/quantum/quantum.c b/quantum/quantum.c
index 4f21e6eea9..0af3f62c4d 100644
--- a/quantum/quantum.c
+++ b/quantum/quantum.c
@@ -55,6 +55,10 @@ float default_layer_songs[][16][2] = DEFAULT_LAYER_SONGS;
# endif
#endif
+#ifdef AUTO_SHIFT_ENABLE
+# include "process_auto_shift.h"
+#endif
+
static void do_code16(uint16_t code, void (*f)(uint8_t)) {
switch (code) {
case QK_MODS ... QK_MODS_MAX:
@@ -228,6 +232,9 @@ bool process_record_quantum(keyrecord_t *record) {
process_record_via(keycode, record) &&
#endif
process_record_kb(keycode, record) &&
+#if defined(SEQUENCER_ENABLE)
+ process_sequencer(keycode, record) &&
+#endif
#if defined(MIDI_ENABLE) && defined(MIDI_ADVANCED)
process_midi(keycode, record) &&
#endif
@@ -649,6 +656,10 @@ void matrix_scan_quantum() {
matrix_scan_music();
#endif
+#ifdef SEQUENCER_ENABLE
+ matrix_scan_sequencer();
+#endif
+
#ifdef TAP_DANCE_ENABLE
matrix_scan_tap_dance();
#endif
@@ -677,6 +688,10 @@ void matrix_scan_quantum() {
dip_switch_read(false);
#endif
+#ifdef AUTO_SHIFT_ENABLE
+ autoshift_matrix_scan();
+#endif
+
matrix_scan_kb();
}
diff --git a/quantum/quantum.h b/quantum/quantum.h
index 2c06578652..94188a8543 100644
--- a/quantum/quantum.h
+++ b/quantum/quantum.h
@@ -31,7 +31,7 @@
#ifdef BACKLIGHT_ENABLE
# ifdef LED_MATRIX_ENABLE
-# include "ledmatrix.h"
+# include "led_matrix.h"
# else
# include "backlight.h"
# endif
@@ -68,6 +68,11 @@ extern layer_state_t default_layer_state;
extern layer_state_t layer_state;
#endif
+#if defined(SEQUENCER_ENABLE)
+# include "sequencer.h"
+# include "process_sequencer.h"
+#endif
+
#if defined(MIDI_ENABLE) && defined(MIDI_ADVANCED)
# include "process_midi.h"
#endif
@@ -229,6 +234,61 @@ typedef ioline_t pin_t;
# define togglePin(pin) palToggleLine(pin)
#endif
+// Atomic macro to help make GPIO and other controls atomic.
+#ifdef IGNORE_ATOMIC_BLOCK
+/* do nothing atomic macro */
+# define ATOMIC_BLOCK for (uint8_t __ToDo = 1; __ToDo; __ToDo = 0)
+# define ATOMIC_BLOCK_RESTORESTATE ATOMIC_BLOCK
+# define ATOMIC_BLOCK_FORCEON ATOMIC_BLOCK
+
+#elif defined(__AVR__)
+/* atomic macro for AVR */
+# include <util/atomic.h>
+
+# define ATOMIC_BLOCK_RESTORESTATE ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
+# define ATOMIC_BLOCK_FORCEON ATOMIC_BLOCK(ATOMIC_FORCEON)
+
+#elif defined(PROTOCOL_CHIBIOS) || defined(PROTOCOL_ARM_ATSAM)
+/* atomic macro for ChibiOS / ARM ATSAM */
+# if defined(PROTOCOL_ARM_ATSAM)
+# include "arm_atsam_protocol.h"
+# endif
+
+static __inline__ uint8_t __interrupt_disable__(void) {
+# if defined(PROTOCOL_CHIBIOS)
+ chSysLock();
+# endif
+# if defined(PROTOCOL_ARM_ATSAM)
+ __disable_irq();
+# endif
+ return 1;
+}
+
+static __inline__ void __interrupt_enable__(const uint8_t *__s) {
+# if defined(PROTOCOL_CHIBIOS)
+ chSysUnlock();
+# endif
+# if defined(PROTOCOL_ARM_ATSAM)
+ __enable_irq();
+# endif
+ __asm__ volatile("" ::: "memory");
+ (void)__s;
+}
+
+# define ATOMIC_BLOCK(type) for (type, __ToDo = __interrupt_disable__(); __ToDo; __ToDo = 0)
+# define ATOMIC_FORCEON uint8_t sreg_save __attribute__((__cleanup__(__interrupt_enable__))) = 0
+
+# define ATOMIC_BLOCK_RESTORESTATE _Static_assert(0, "ATOMIC_BLOCK_RESTORESTATE dose not implement")
+# define ATOMIC_BLOCK_FORCEON ATOMIC_BLOCK(ATOMIC_FORCEON)
+
+/* Other platform */
+#else
+
+# define ATOMIC_BLOCK_RESTORESTATE _Static_assert(0, "ATOMIC_BLOCK_RESTORESTATE dose not implement")
+# define ATOMIC_BLOCK_FORCEON _Static_assert(0, "ATOMIC_BLOCK_FORCEON dose not implement")
+
+#endif
+
#define SEND_STRING(string) send_string_P(PSTR(string))
#define SEND_STRING_DELAY(string, interval) send_string_with_delay_P(PSTR(string), interval)
diff --git a/quantum/quantum_keycodes.h b/quantum/quantum_keycodes.h
index 38618964e3..34102aab2d 100644
--- a/quantum/quantum_keycodes.h
+++ b/quantum/quantum_keycodes.h
@@ -16,6 +16,10 @@
#ifndef QUANTUM_KEYCODES_H
#define QUANTUM_KEYCODES_H
+#if defined(SEQUENCER_ENABLE)
+# include "sequencer.h"
+#endif
+
#ifndef MIDI_ENABLE_STRICT
# define MIDI_ENABLE_STRICT 0
#endif
@@ -343,7 +347,8 @@ enum quantum_keycodes {
MI_TRNSU, // transpose up
MIDI_VELOCITY_MIN,
- MI_VEL_1 = MIDI_VELOCITY_MIN,
+ MI_VEL_0 = MIDI_VELOCITY_MIN,
+ MI_VEL_1,
MI_VEL_2,
MI_VEL_3,
MI_VEL_4,
@@ -551,6 +556,37 @@ enum quantum_keycodes {
JS_BUTTON31,
JS_BUTTON_MAX = JS_BUTTON31,
+#if defined(SEQUENCER_ENABLE)
+ SQ_ON,
+ SQ_OFF,
+ SQ_TOG,
+
+ SQ_TMPD, // Decrease tempo
+ SQ_TMPU, // Increase tempo
+
+ SEQUENCER_RESOLUTION_MIN,
+ SEQUENCER_RESOLUTION_MAX = SEQUENCER_RESOLUTION_MIN + SEQUENCER_RESOLUTIONS,
+ SQ_RESD, // Decrease resolution
+ SQ_RESU, // Increase resolution
+
+ SQ_SALL, // All steps on
+ SQ_SCLR, // All steps off
+ SEQUENCER_STEP_MIN,
+ SEQUENCER_STEP_MAX = SEQUENCER_STEP_MIN + SEQUENCER_STEPS,
+
+ SEQUENCER_TRACK_MIN,
+ SEQUENCER_TRACK_MAX = SEQUENCER_TRACK_MIN + SEQUENCER_TRACKS,
+
+/**
+ * Helpers to assign a keycode to a step, a resolution, or a track.
+ * Falls back to NOOP if n is out of range.
+ */
+# define SQ_S(n) (n < SEQUENCER_STEPS ? SEQUENCER_STEP_MIN + n : XXXXXXX)
+# define SQ_R(n) (n < SEQUENCER_RESOLUTIONS ? SEQUENCER_RESOLUTION_MIN + n : XXXXXXX)
+# define SQ_T(n) (n < SEQUENCER_TRACKS ? SEQUENCER_TRACK_MIN + n : XXXXXXX)
+
+#endif
+
// always leave at the end
SAFE_RANGE
};
diff --git a/quantum/rgb_matrix.c b/quantum/rgb_matrix.c
index 2a55a8926f..404e5bc1f9 100644
--- a/quantum/rgb_matrix.c
+++ b/quantum/rgb_matrix.c
@@ -31,6 +31,8 @@ const point_t k_rgb_matrix_center = {112, 32};
const point_t k_rgb_matrix_center = RGB_MATRIX_CENTER;
#endif
+__attribute__((weak)) RGB rgb_matrix_hsv_to_rgb(HSV hsv) { return hsv_to_rgb(hsv); }
+
// Generic effect runners
#include "rgb_matrix_runners/effect_runner_dx_dy_dist.h"
#include "rgb_matrix_runners/effect_runner_dx_dy.h"
@@ -402,6 +404,10 @@ void rgb_matrix_task(void) {
break;
case RENDERING:
rgb_task_render(effect);
+ if (!suspend_backlight) {
+ rgb_matrix_indicators();
+ rgb_matrix_indicators_advanced(&rgb_effect_params);
+ }
break;
case FLUSHING:
rgb_task_flush(effect);
@@ -410,10 +416,6 @@ void rgb_matrix_task(void) {
rgb_task_sync();
break;
}
-
- if (!suspend_backlight) {
- rgb_matrix_indicators();
- }
}
void rgb_matrix_indicators(void) {
@@ -425,6 +427,28 @@ __attribute__((weak)) void rgb_matrix_indicators_kb(void) {}
__attribute__((weak)) void rgb_matrix_indicators_user(void) {}
+void rgb_matrix_indicators_advanced(effect_params_t *params) {
+ /* special handling is needed for "params->iter", since it's already been incremented.
+ * Could move the invocations to rgb_task_render, but then it's missing a few checks
+ * and not sure which would be better. Otherwise, this should be called from
+ * rgb_task_render, right before the iter++ line.
+ */
+#if defined(RGB_MATRIX_LED_PROCESS_LIMIT) && RGB_MATRIX_LED_PROCESS_LIMIT > 0 && RGB_MATRIX_LED_PROCESS_LIMIT < DRIVER_LED_TOTAL
+ uint8_t min = RGB_MATRIX_LED_PROCESS_LIMIT * (params->iter - 1);
+ uint8_t max = min + RGB_MATRIX_LED_PROCESS_LIMIT;
+ if (max > DRIVER_LED_TOTAL) max = DRIVER_LED_TOTAL;
+#else
+ uint8_t min = 0;
+ uint8_t max = DRIVER_LED_TOTAL;
+#endif
+ rgb_matrix_indicators_advanced_kb(min, max);
+ rgb_matrix_indicators_advanced_user(min, max);
+}
+
+__attribute__((weak)) void rgb_matrix_indicators_advanced_kb(uint8_t led_min, uint8_t led_max) {}
+
+__attribute__((weak)) void rgb_matrix_indicators_advanced_user(uint8_t led_min, uint8_t led_max) {}
+
void rgb_matrix_init(void) {
rgb_matrix_driver.init();
diff --git a/quantum/rgb_matrix.h b/quantum/rgb_matrix.h
index 733333349f..771a1fcd35 100644
--- a/quantum/rgb_matrix.h
+++ b/quantum/rgb_matrix.h
@@ -57,6 +57,11 @@
uint8_t max = DRIVER_LED_TOTAL;
#endif
+#define RGB_MATRIX_INDICATOR_SET_COLOR(i, r, g, b) \
+ if (i >= led_min && i <= led_max) { \
+ rgb_matrix_set_color(i, r, g, b); \
+ }
+
#define RGB_MATRIX_TEST_LED_FLAGS() \
if (!HAS_ANY_FLAGS(g_led_config.flags[i], params->flags)) continue
@@ -103,6 +108,10 @@ void rgb_matrix_indicators(void);
void rgb_matrix_indicators_kb(void);
void rgb_matrix_indicators_user(void);
+void rgb_matrix_indicators_advanced(effect_params_t *params);
+void rgb_matrix_indicators_advanced_kb(uint8_t led_min, uint8_t led_max);
+void rgb_matrix_indicators_advanced_user(uint8_t led_min, uint8_t led_max);
+
void rgb_matrix_init(void);
void rgb_matrix_set_suspend_state(bool state);
diff --git a/quantum/rgb_matrix_animations/alpha_mods_anim.h b/quantum/rgb_matrix_animations/alpha_mods_anim.h
index 0778ab2098..426d88ef35 100644
--- a/quantum/rgb_matrix_animations/alpha_mods_anim.h
+++ b/quantum/rgb_matrix_animations/alpha_mods_anim.h
@@ -7,9 +7,9 @@ bool ALPHAS_MODS(effect_params_t* params) {
RGB_MATRIX_USE_LIMITS(led_min, led_max);
HSV hsv = rgb_matrix_config.hsv;
- RGB rgb1 = hsv_to_rgb(hsv);
+ RGB rgb1 = rgb_matrix_hsv_to_rgb(hsv);
hsv.h += rgb_matrix_config.speed;
- RGB rgb2 = hsv_to_rgb(hsv);
+ RGB rgb2 = rgb_matrix_hsv_to_rgb(hsv);
for (uint8_t i = led_min; i < led_max; i++) {
RGB_MATRIX_TEST_LED_FLAGS();
diff --git a/quantum/rgb_matrix_animations/breathing_anim.h b/quantum/rgb_matrix_animations/breathing_anim.h
index 887425f9da..340bd93e5d 100644
--- a/quantum/rgb_matrix_animations/breathing_anim.h
+++ b/quantum/rgb_matrix_animations/breathing_anim.h
@@ -8,7 +8,7 @@ bool BREATHING(effect_params_t* params) {
HSV hsv = rgb_matrix_config.hsv;
uint16_t time = scale16by8(g_rgb_timer, rgb_matrix_config.speed / 8);
hsv.v = scale8(abs8(sin8(time) - 128) * 2, hsv.v);
- RGB rgb = hsv_to_rgb(hsv);
+ RGB rgb = rgb_matrix_hsv_to_rgb(hsv);
for (uint8_t i = led_min; i < led_max; i++) {
RGB_MATRIX_TEST_LED_FLAGS();
rgb_matrix_set_color(i, rgb.r, rgb.g, rgb.b);
diff --git a/quantum/rgb_matrix_animations/gradient_left_right_anim.h b/quantum/rgb_matrix_animations/gradient_left_right_anim.h
index 2eab2eb759..53dfd04e2c 100644
--- a/quantum/rgb_matrix_animations/gradient_left_right_anim.h
+++ b/quantum/rgb_matrix_animations/gradient_left_right_anim.h
@@ -12,7 +12,7 @@ bool GRADIENT_LEFT_RIGHT(effect_params_t* params) {
// The x range will be 0..224, map this to 0..7
// Relies on hue being 8-bit and wrapping
hsv.h = rgb_matrix_config.hsv.h + (scale * g_led_config.point[i].x >> 5);
- RGB rgb = hsv_to_rgb(hsv);
+ RGB rgb = rgb_matrix_hsv_to_rgb(hsv);
rgb_matrix_set_color(i, rgb.r, rgb.g, rgb.b);
}
return led_max < DRIVER_LED_TOTAL;
diff --git a/quantum/rgb_matrix_animations/gradient_up_down_anim.h b/quantum/rgb_matrix_animations/gradient_up_down_anim.h
index 0f1f8e23cf..7e0d2898cf 100644
--- a/quantum/rgb_matrix_animations/gradient_up_down_anim.h
+++ b/quantum/rgb_matrix_animations/gradient_up_down_anim.h
@@ -12,7 +12,7 @@ bool GRADIENT_UP_DOWN(effect_params_t* params) {
// The y range will be 0..64, map this to 0..4
// Relies on hue being 8-bit and wrapping
hsv.h = rgb_matrix_config.hsv.h + scale * (g_led_config.point[i].y >> 4);
- RGB rgb = hsv_to_rgb(hsv);
+ RGB rgb = rgb_matrix_hsv_to_rgb(hsv);
rgb_matrix_set_color(i, rgb.r, rgb.g, rgb.b);
}
return led_max < DRIVER_LED_TOTAL;
diff --git a/quantum/rgb_matrix_animations/jellybean_raindrops_anim.h b/quantum/rgb_matrix_animations/jellybean_raindrops_anim.h
index ef2d1500b0..9493b38508 100644
--- a/quantum/rgb_matrix_animations/jellybean_raindrops_anim.h
+++ b/quantum/rgb_matrix_animations/jellybean_raindrops_anim.h
@@ -5,7 +5,7 @@ RGB_MATRIX_EFFECT(JELLYBEAN_RAINDROPS)
static void jellybean_raindrops_set_color(int i, effect_params_t* params) {
if (!HAS_ANY_FLAGS(g_led_config.flags[i], params->flags)) return;
HSV hsv = {rand() & 0xFF, rand() & 0xFF, rgb_matrix_config.hsv.v};
- RGB rgb = hsv_to_rgb(hsv);
+ RGB rgb = rgb_matrix_hsv_to_rgb(hsv);
rgb_matrix_set_color(i, rgb.r, rgb.g, rgb.b);
}
diff --git a/quantum/rgb_matrix_animations/raindrops_anim.h b/quantum/rgb_matrix_animations/raindrops_anim.h
index 6e1b5acb0d..38359cdca7 100644
--- a/quantum/rgb_matrix_animations/raindrops_anim.h
+++ b/quantum/rgb_matrix_animations/raindrops_anim.h
@@ -15,7 +15,7 @@ static void raindrops_set_color(int i, effect_params_t* params) {
}
hsv.h = rgb_matrix_config.hsv.h + (deltaH * (rand() & 0x03));
- RGB rgb = hsv_to_rgb(hsv);
+ RGB rgb = rgb_matrix_hsv_to_rgb(hsv);
rgb_matrix_set_color(i, rgb.r, rgb.g, rgb.b);
}
diff --git a/quantum/rgb_matrix_animations/solid_color_anim.h b/quantum/rgb_matrix_animations/solid_color_anim.h
index c8f5e70e7a..79d63cf133 100644
--- a/quantum/rgb_matrix_animations/solid_color_anim.h
+++ b/quantum/rgb_matrix_animations/solid_color_anim.h
@@ -4,7 +4,7 @@ RGB_MATRIX_EFFECT(SOLID_COLOR)
bool SOLID_COLOR(effect_params_t* params) {
RGB_MATRIX_USE_LIMITS(led_min, led_max);
- RGB rgb = hsv_to_rgb(rgb_matrix_config.hsv);
+ RGB rgb = rgb_matrix_hsv_to_rgb(rgb_matrix_config.hsv);
for (uint8_t i = led_min; i < led_max; i++) {
RGB_MATRIX_TEST_LED_FLAGS();
rgb_matrix_set_color(i, rgb.r, rgb.g, rgb.b);
diff --git a/quantum/rgb_matrix_animations/typing_heatmap_anim.h b/quantum/rgb_matrix_animations/typing_heatmap_anim.h
index e82c1b49ee..b855fdc190 100644
--- a/quantum/rgb_matrix_animations/typing_heatmap_anim.h
+++ b/quantum/rgb_matrix_animations/typing_heatmap_anim.h
@@ -51,7 +51,7 @@ bool TYPING_HEATMAP(effect_params_t* params) {
if (!HAS_ANY_FLAGS(g_led_config.flags[led[j]], params->flags)) continue;
HSV hsv = {170 - qsub8(val, 85), rgb_matrix_config.hsv.s, scale8((qadd8(170, val) - 170) * 3, rgb_matrix_config.hsv.v)};
- RGB rgb = hsv_to_rgb(hsv);
+ RGB rgb = rgb_matrix_hsv_to_rgb(hsv);
rgb_matrix_set_color(led[j], rgb.r, rgb.g, rgb.b);
}
diff --git a/quantum/rgb_matrix_runners/effect_runner_dx_dy.h b/quantum/rgb_matrix_runners/effect_runner_dx_dy.h
index 9d0c9fab19..4867609c81 100644
--- a/quantum/rgb_matrix_runners/effect_runner_dx_dy.h
+++ b/quantum/rgb_matrix_runners/effect_runner_dx_dy.h
@@ -10,7 +10,7 @@ bool effect_runner_dx_dy(effect_params_t* params, dx_dy_f effect_func) {
RGB_MATRIX_TEST_LED_FLAGS();
int16_t dx = g_led_config.point[i].x - k_rgb_matrix_center.x;
int16_t dy = g_led_config.point[i].y - k_rgb_matrix_center.y;
- RGB rgb = hsv_to_rgb(effect_func(rgb_matrix_config.hsv, dx, dy, time));
+ RGB rgb = rgb_matrix_hsv_to_rgb(effect_func(rgb_matrix_config.hsv, dx, dy, time));
rgb_matrix_set_color(i, rgb.r, rgb.g, rgb.b);
}
return led_max < DRIVER_LED_TOTAL;
diff --git a/quantum/rgb_matrix_runners/effect_runner_dx_dy_dist.h b/quantum/rgb_matrix_runners/effect_runner_dx_dy_dist.h
index 2824c82527..9545b418d9 100644
--- a/quantum/rgb_matrix_runners/effect_runner_dx_dy_dist.h
+++ b/quantum/rgb_matrix_runners/effect_runner_dx_dy_dist.h
@@ -11,7 +11,7 @@ bool effect_runner_dx_dy_dist(effect_params_t* params, dx_dy_dist_f effect_func)
int16_t dx = g_led_config.point[i].x - k_rgb_matrix_center.x;
int16_t dy = g_led_config.point[i].y - k_rgb_matrix_center.y;
uint8_t dist = sqrt16(dx * dx + dy * dy);
- RGB rgb = hsv_to_rgb(effect_func(rgb_matrix_config.hsv, dx, dy, dist, time));
+ RGB rgb = rgb_matrix_hsv_to_rgb(effect_func(rgb_matrix_config.hsv, dx, dy, dist, time));
rgb_matrix_set_color(i, rgb.r, rgb.g, rgb.b);
}
return led_max < DRIVER_LED_TOTAL;
diff --git a/quantum/rgb_matrix_runners/effect_runner_i.h b/quantum/rgb_matrix_runners/effect_runner_i.h
index 5e6bf5daaf..95bfe8b390 100644
--- a/quantum/rgb_matrix_runners/effect_runner_i.h
+++ b/quantum/rgb_matrix_runners/effect_runner_i.h
@@ -8,7 +8,7 @@ bool effect_runner_i(effect_params_t* params, i_f effect_func) {
uint8_t time = scale16by8(g_rgb_timer, rgb_matrix_config.speed / 4);
for (uint8_t i = led_min; i < led_max; i++) {
RGB_MATRIX_TEST_LED_FLAGS();
- RGB rgb = hsv_to_rgb(effect_func(rgb_matrix_config.hsv, i, time));
+ RGB rgb = rgb_matrix_hsv_to_rgb(effect_func(rgb_matrix_config.hsv, i, time));
rgb_matrix_set_color(i, rgb.r, rgb.g, rgb.b);
}
return led_max < DRIVER_LED_TOTAL;
diff --git a/quantum/rgb_matrix_runners/effect_runner_reactive.h b/quantum/rgb_matrix_runners/effect_runner_reactive.h
index 53e77e3fb2..8485b61f3d 100644
--- a/quantum/rgb_matrix_runners/effect_runner_reactive.h
+++ b/quantum/rgb_matrix_runners/effect_runner_reactive.h
@@ -20,7 +20,7 @@ bool effect_runner_reactive(effect_params_t* params, reactive_f effect_func) {
}
uint16_t offset = scale16by8(tick, rgb_matrix_config.speed);
- RGB rgb = hsv_to_rgb(effect_func(rgb_matrix_config.hsv, offset));
+ RGB rgb = rgb_matrix_hsv_to_rgb(effect_func(rgb_matrix_config.hsv, offset));
rgb_matrix_set_color(i, rgb.r, rgb.g, rgb.b);
}
return led_max < DRIVER_LED_TOTAL;
diff --git a/quantum/rgb_matrix_runners/effect_runner_reactive_splash.h b/quantum/rgb_matrix_runners/effect_runner_reactive_splash.h
index b5d284a40f..5c69d0fbb9 100644
--- a/quantum/rgb_matrix_runners/effect_runner_reactive_splash.h
+++ b/quantum/rgb_matrix_runners/effect_runner_reactive_splash.h
@@ -20,7 +20,7 @@ bool effect_runner_reactive_splash(uint8_t start, effect_params_t* params, react
hsv = effect_func(hsv, dx, dy, dist, tick);
}
hsv.v = scale8(hsv.v, rgb_matrix_config.hsv.v);
- RGB rgb = hsv_to_rgb(hsv);
+ RGB rgb = rgb_matrix_hsv_to_rgb(hsv);
rgb_matrix_set_color(i, rgb.r, rgb.g, rgb.b);
}
return led_max < DRIVER_LED_TOTAL;
diff --git a/quantum/rgb_matrix_runners/effect_runner_sin_cos_i.h b/quantum/rgb_matrix_runners/effect_runner_sin_cos_i.h
index 3fb7d48051..02351de51e 100644
--- a/quantum/rgb_matrix_runners/effect_runner_sin_cos_i.h
+++ b/quantum/rgb_matrix_runners/effect_runner_sin_cos_i.h
@@ -10,7 +10,7 @@ bool effect_runner_sin_cos_i(effect_params_t* params, sin_cos_i_f effect_func) {
int8_t sin_value = sin8(time) - 128;
for (uint8_t i = led_min; i < led_max; i++) {
RGB_MATRIX_TEST_LED_FLAGS();
- RGB rgb = hsv_to_rgb(effect_func(rgb_matrix_config.hsv, cos_value, sin_value, i, time));
+ RGB rgb = rgb_matrix_hsv_to_rgb(effect_func(rgb_matrix_config.hsv, cos_value, sin_value, i, time));
rgb_matrix_set_color(i, rgb.r, rgb.g, rgb.b);
}
return led_max < DRIVER_LED_TOTAL;
diff --git a/quantum/rgblight.c b/quantum/rgblight.c
index 5ed3e8ac5f..4aa5cb4ac9 100644
--- a/quantum/rgblight.c
+++ b/quantum/rgblight.c
@@ -123,9 +123,11 @@ void rgblight_set_effect_range(uint8_t start_pos, uint8_t num_leds) {
rgblight_ranges.effect_num_leds = num_leds;
}
+__attribute__((weak)) RGB rgblight_hsv_to_rgb(HSV hsv) { return hsv_to_rgb(hsv); }
+
void sethsv_raw(uint8_t hue, uint8_t sat, uint8_t val, LED_TYPE *led1) {
HSV hsv = {hue, sat, val};
- RGB rgb = hsv_to_rgb(hsv);
+ RGB rgb = rgblight_hsv_to_rgb(hsv);
setrgb(rgb.r, rgb.g, rgb.b, led1);
}
diff --git a/quantum/sequencer/sequencer.c b/quantum/sequencer/sequencer.c
new file mode 100644
index 0000000000..0eaf3a17aa
--- /dev/null
+++ b/quantum/sequencer/sequencer.c
@@ -0,0 +1,275 @@
+/* Copyright 2020 Rodolphe Belouin
+ *
+ * This program 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "sequencer.h"
+
+#ifdef MIDI_ENABLE
+# include "process_midi.h"
+#endif
+
+#ifdef MIDI_MOCKED
+# include "tests/midi_mock.h"
+#endif
+
+sequencer_config_t sequencer_config = {
+ false, // enabled
+ {false}, // steps
+ {0}, // track notes
+ 60, // tempo
+ SQ_RES_4, // resolution
+};
+
+sequencer_state_t sequencer_internal_state = {0, 0, 0, 0, SEQUENCER_PHASE_ATTACK};
+
+bool is_sequencer_on(void) { return sequencer_config.enabled; }
+
+void sequencer_on(void) {
+ dprintln("sequencer on");
+ sequencer_config.enabled = true;
+ sequencer_internal_state.current_track = 0;
+ sequencer_internal_state.current_step = 0;
+ sequencer_internal_state.timer = timer_read();
+ sequencer_internal_state.phase = SEQUENCER_PHASE_ATTACK;
+}
+
+void sequencer_off(void) {
+ dprintln("sequencer off");
+ sequencer_config.enabled = false;
+ sequencer_internal_state.current_step = 0;
+}
+
+void sequencer_toggle(void) {
+ if (is_sequencer_on()) {
+ sequencer_off();
+ } else {
+ sequencer_on();
+ }
+}
+
+void sequencer_set_track_notes(const uint16_t track_notes[SEQUENCER_TRACKS]) {
+ for (uint8_t i = 0; i < SEQUENCER_TRACKS; i++) {
+ sequencer_config.track_notes[i] = track_notes[i];
+ }
+}
+
+bool is_sequencer_track_active(uint8_t track) { return (sequencer_internal_state.active_tracks >> track) & true; }
+
+void sequencer_set_track_activation(uint8_t track, bool value) {
+ if (value) {
+ sequencer_internal_state.active_tracks |= (1 << track);
+ } else {
+ sequencer_internal_state.active_tracks &= ~(1 << track);
+ }
+ dprintf("sequencer: track %d is %s\n", track, value ? "active" : "inactive");
+}
+
+void sequencer_toggle_track_activation(uint8_t track) { sequencer_set_track_activation(track, !is_sequencer_track_active(track)); }
+
+void sequencer_toggle_single_active_track(uint8_t track) {
+ if (is_sequencer_track_active(track)) {
+ sequencer_internal_state.active_tracks = 0;
+ } else {
+ sequencer_internal_state.active_tracks = 1 << track;
+ }
+}
+
+bool is_sequencer_step_on(uint8_t step) { return step < SEQUENCER_STEPS && (sequencer_config.steps[step] & sequencer_internal_state.active_tracks) > 0; }
+
+bool is_sequencer_step_on_for_track(uint8_t step, uint8_t track) { return step < SEQUENCER_STEPS && (sequencer_config.steps[step] >> track) & true; }
+
+void sequencer_set_step(uint8_t step, bool value) {
+ if (step < SEQUENCER_STEPS) {
+ if (value) {
+ sequencer_config.steps[step] |= sequencer_internal_state.active_tracks;
+ } else {
+ sequencer_config.steps[step] &= ~sequencer_internal_state.active_tracks;
+ }
+ dprintf("sequencer: step %d is %s\n", step, value ? "on" : "off");
+ } else {
+ dprintf("sequencer: step %d is out of range\n", step);
+ }
+}
+
+void sequencer_toggle_step(uint8_t step) {
+ if (is_sequencer_step_on(step)) {
+ sequencer_set_step_off(step);
+ } else {
+ sequencer_set_step_on(step);
+ }
+}
+
+void sequencer_set_all_steps(bool value) {
+ for (uint8_t step = 0; step < SEQUENCER_STEPS; step++) {
+ if (value) {
+ sequencer_config.steps[step] |= sequencer_internal_state.active_tracks;
+ } else {
+ sequencer_config.steps[step] &= ~sequencer_internal_state.active_tracks;
+ }
+ }
+ dprintf("sequencer: all steps are %s\n", value ? "on" : "off");
+}
+
+uint8_t sequencer_get_tempo(void) { return sequencer_config.tempo; }
+
+void sequencer_set_tempo(uint8_t tempo) {
+ if (tempo > 0) {
+ sequencer_config.tempo = tempo;
+ dprintf("sequencer: tempo set to %d bpm\n", tempo);
+ } else {
+ dprintln("sequencer: cannot set tempo to 0");
+ }
+}
+
+void sequencer_increase_tempo(void) {
+ // Handling potential uint8_t overflow
+ if (sequencer_config.tempo < UINT8_MAX) {
+ sequencer_set_tempo(sequencer_config.tempo + 1);
+ } else {
+ dprintf("sequencer: cannot set tempo above %d\n", UINT8_MAX);
+ }
+}
+
+void sequencer_decrease_tempo(void) { sequencer_set_tempo(sequencer_config.tempo - 1); }
+
+sequencer_resolution_t sequencer_get_resolution(void) { return sequencer_config.resolution; }
+
+void sequencer_set_resolution(sequencer_resolution_t resolution) {
+ if (resolution >= 0 && resolution < SEQUENCER_RESOLUTIONS) {
+ sequencer_config.resolution = resolution;
+ dprintf("sequencer: resolution set to %d\n", resolution);
+ } else {
+ dprintf("sequencer: resolution %d is out of range\n", resolution);
+ }
+}
+
+void sequencer_increase_resolution(void) { sequencer_set_resolution(sequencer_config.resolution + 1); }
+
+void sequencer_decrease_resolution(void) { sequencer_set_resolution(sequencer_config.resolution - 1); }
+
+uint8_t sequencer_get_current_step(void) { return sequencer_internal_state.current_step; }
+
+void sequencer_phase_attack(void) {
+ dprintf("sequencer: step %d\n", sequencer_internal_state.current_step);
+ dprintf("sequencer: time %d\n", timer_read());
+
+ if (sequencer_internal_state.current_track == 0) {
+ sequencer_internal_state.timer = timer_read();
+ }
+
+ if (timer_elapsed(sequencer_internal_state.timer) < sequencer_internal_state.current_track * SEQUENCER_TRACK_THROTTLE) {
+ return;
+ }
+
+#if defined(MIDI_ENABLE) || defined(MIDI_MOCKED)
+ if (is_sequencer_step_on_for_track(sequencer_internal_state.current_step, sequencer_internal_state.current_track)) {
+ process_midi_basic_noteon(midi_compute_note(sequencer_config.track_notes[sequencer_internal_state.current_track]));
+ }
+#endif
+
+ if (sequencer_internal_state.current_track < SEQUENCER_TRACKS - 1) {
+ sequencer_internal_state.current_track++;
+ } else {
+ sequencer_internal_state.phase = SEQUENCER_PHASE_RELEASE;
+ }
+}
+
+void sequencer_phase_release(void) {
+ if (timer_elapsed(sequencer_internal_state.timer) < SEQUENCER_PHASE_RELEASE_TIMEOUT + sequencer_internal_state.current_track * SEQUENCER_TRACK_THROTTLE) {
+ return;
+ }
+#if defined(MIDI_ENABLE) || defined(MIDI_MOCKED)
+ if (is_sequencer_step_on_for_track(sequencer_internal_state.current_step, sequencer_internal_state.current_track)) {
+ process_midi_basic_noteoff(midi_compute_note(sequencer_config.track_notes[sequencer_internal_state.current_track]));
+ }
+#endif
+ if (sequencer_internal_state.current_track > 0) {
+ sequencer_internal_state.current_track--;
+ } else {
+ sequencer_internal_state.phase = SEQUENCER_PHASE_PAUSE;
+ }
+}
+
+void sequencer_phase_pause(void) {
+ if (timer_elapsed(sequencer_internal_state.timer) < sequencer_get_step_duration()) {
+ return;
+ }
+
+ sequencer_internal_state.current_step = (sequencer_internal_state.current_step + 1) % SEQUENCER_STEPS;
+ sequencer_internal_state.phase = SEQUENCER_PHASE_ATTACK;
+}
+
+void matrix_scan_sequencer(void) {
+ if (!sequencer_config.enabled) {
+ return;
+ }
+
+ if (sequencer_internal_state.phase == SEQUENCER_PHASE_PAUSE) {
+ sequencer_phase_pause();
+ }
+
+ if (sequencer_internal_state.phase == SEQUENCER_PHASE_RELEASE) {
+ sequencer_phase_release();
+ }
+
+ if (sequencer_internal_state.phase == SEQUENCER_PHASE_ATTACK) {
+ sequencer_phase_attack();
+ }
+}
+
+uint16_t sequencer_get_beat_duration(void) { return get_beat_duration(sequencer_config.tempo); }
+
+uint16_t sequencer_get_step_duration(void) { return get_step_duration(sequencer_config.tempo, sequencer_config.resolution); }
+
+uint16_t get_beat_duration(uint8_t tempo) {
+ // Don’t crash in the unlikely case where the given tempo is 0
+ if (tempo == 0) {
+ return get_beat_duration(60);
+ }
+
+ /**
+ * Given
+ * t = tempo and d = duration, both strictly greater than 0
+ * When
+ * t beats / minute = 1 beat / d ms
+ * Then
+ * t beats / 60000ms = 1 beat / d ms
+ * d ms = 60000ms / t
+ */
+ return 60000 / tempo;
+}
+
+uint16_t get_step_duration(uint8_t tempo, sequencer_resolution_t resolution) {
+ /**
+ * Resolution cheatsheet:
+ * 1/2 => 2 steps per 4 beats
+ * 1/2T => 3 steps per 4 beats
+ * 1/4 => 4 steps per 4 beats
+ * 1/4T => 6 steps per 4 beats
+ * 1/8 => 8 steps per 4 beats
+ * 1/8T => 12 steps per 4 beats
+ * 1/16 => 16 steps per 4 beats
+ * 1/16T => 24 steps per 4 beats
+ * 1/32 => 32 steps per 4 beats
+ *
+ * The number of steps for binary resolutions follows the powers of 2.
+ * The ternary variants are simply 1.5x faster.
+ */
+ bool is_binary = resolution % 2 == 0;
+ uint8_t binary_steps = 2 << (resolution / 2);
+ uint16_t binary_step_duration = get_beat_duration(tempo) * 4 / binary_steps;
+
+ return is_binary ? binary_step_duration : 2 * binary_step_duration / 3;
+}
diff --git a/quantum/sequencer/sequencer.h b/quantum/sequencer/sequencer.h
new file mode 100644
index 0000000000..aeca7a1e9b
--- /dev/null
+++ b/quantum/sequencer/sequencer.h
@@ -0,0 +1,122 @@
+/* Copyright 2020 Rodolphe Belouin
+ *
+ * This program 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+#include "debug.h"
+#include "timer.h"
+
+// Maximum number of steps: 256
+#ifndef SEQUENCER_STEPS
+# define SEQUENCER_STEPS 16
+#endif
+
+// Maximum number of tracks: 8
+#ifndef SEQUENCER_TRACKS
+# define SEQUENCER_TRACKS 8
+#endif
+
+#ifndef SEQUENCER_TRACK_THROTTLE
+# define SEQUENCER_TRACK_THROTTLE 3
+#endif
+
+#ifndef SEQUENCER_PHASE_RELEASE_TIMEOUT
+# define SEQUENCER_PHASE_RELEASE_TIMEOUT 30
+#endif
+
+/**
+ * Make sure that the items of this enumeration follow the powers of 2, separated by a ternary variant.
+ * Check the implementation of `get_step_duration` for further explanation.
+ */
+typedef enum { SQ_RES_2, SQ_RES_2T, SQ_RES_4, SQ_RES_4T, SQ_RES_8, SQ_RES_8T, SQ_RES_16, SQ_RES_16T, SQ_RES_32, SEQUENCER_RESOLUTIONS } sequencer_resolution_t;
+
+typedef struct {
+ bool enabled;
+ uint8_t steps[SEQUENCER_STEPS];
+ uint16_t track_notes[SEQUENCER_TRACKS];
+ uint8_t tempo; // Is a maximum tempo of 255 reasonable?
+ sequencer_resolution_t resolution;
+} sequencer_config_t;
+
+/**
+ * Because Digital Audio Workstations get overwhelmed when too many MIDI signals are sent concurrently,
+ * We use a "phase" state machine to delay some of the events.
+ */
+typedef enum sequencer_phase_t {
+ SEQUENCER_PHASE_ATTACK, // t=0ms, send the MIDI note on signal
+ SEQUENCER_PHASE_RELEASE, // t=SEQUENCER_PHASE_RELEASE_TIMEOUT ms, send the MIDI note off signal
+ SEQUENCER_PHASE_PAUSE // t=step duration ms, loop
+} sequencer_phase_t;
+
+typedef struct {
+ uint8_t active_tracks;
+ uint8_t current_track;
+ uint8_t current_step;
+ uint16_t timer;
+ sequencer_phase_t phase;
+} sequencer_state_t;
+
+extern sequencer_config_t sequencer_config;
+
+// We expose the internal state to make the feature more "unit-testable"
+extern sequencer_state_t sequencer_internal_state;
+
+bool is_sequencer_on(void);
+void sequencer_toggle(void);
+void sequencer_on(void);
+void sequencer_off(void);
+
+void sequencer_set_track_notes(const uint16_t track_notes[SEQUENCER_TRACKS]);
+
+bool is_sequencer_track_active(uint8_t track);
+void sequencer_set_track_activation(uint8_t track, bool value);
+void sequencer_toggle_track_activation(uint8_t track);
+void sequencer_toggle_single_active_track(uint8_t track);
+
+#define sequencer_activate_track(track) sequencer_set_track_activation(track, true)
+#define sequencer_deactivate_track(track) sequencer_set_track_activation(track, false)
+
+bool is_sequencer_step_on(uint8_t step);
+bool is_sequencer_step_on_for_track(uint8_t step, uint8_t track);
+void sequencer_set_step(uint8_t step, bool value);
+void sequencer_toggle_step(uint8_t step);
+void sequencer_set_all_steps(bool value);
+
+#define sequencer_set_step_on(step) sequencer_set_step(step, true)
+#define sequencer_set_step_off(step) sequencer_set_step(step, false)
+#define sequencer_set_all_steps_on() sequencer_set_all_steps(true)
+#define sequencer_set_all_steps_off() sequencer_set_all_steps(false)
+
+uint8_t sequencer_get_tempo(void);
+void sequencer_set_tempo(uint8_t tempo);
+void sequencer_increase_tempo(void);
+void sequencer_decrease_tempo(void);
+
+sequencer_resolution_t sequencer_get_resolution(void);
+void sequencer_set_resolution(sequencer_resolution_t resolution);
+void sequencer_increase_resolution(void);
+void sequencer_decrease_resolution(void);
+
+uint8_t sequencer_get_current_step(void);
+
+uint16_t sequencer_get_beat_duration(void);
+uint16_t sequencer_get_step_duration(void);
+
+uint16_t get_beat_duration(uint8_t tempo);
+uint16_t get_step_duration(uint8_t tempo, sequencer_resolution_t resolution);
+
+void matrix_scan_sequencer(void);
diff --git a/quantum/sequencer/tests/midi_mock.c b/quantum/sequencer/tests/midi_mock.c
new file mode 100644
index 0000000000..236e16f9d7
--- /dev/null
+++ b/quantum/sequencer/tests/midi_mock.c
@@ -0,0 +1,26 @@
+/* Copyright 2020 Rodolphe Belouin
+ *
+ * This program 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "midi_mock.h"
+
+uint16_t last_noteon = 0;
+uint16_t last_noteoff = 0;
+
+uint16_t midi_compute_note(uint16_t keycode) { return keycode; }
+
+void process_midi_basic_noteon(uint16_t note) { last_noteon = note; }
+
+void process_midi_basic_noteoff(uint16_t note) { last_noteoff = note; }
diff --git a/quantum/sequencer/tests/midi_mock.h b/quantum/sequencer/tests/midi_mock.h
new file mode 100644
index 0000000000..4d8c2eb307
--- /dev/null
+++ b/quantum/sequencer/tests/midi_mock.h
@@ -0,0 +1,26 @@
+/* Copyright 2020 Rodolphe Belouin
+ *
+ * This program 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+extern uint16_t last_noteon;
+extern uint16_t last_noteoff;
+
+uint16_t midi_compute_note(uint16_t keycode);
+void process_midi_basic_noteon(uint16_t note);
+void process_midi_basic_noteoff(uint16_t note);
diff --git a/quantum/sequencer/tests/rules.mk b/quantum/sequencer/tests/rules.mk
new file mode 100644
index 0000000000..76c221cf92
--- /dev/null
+++ b/quantum/sequencer/tests/rules.mk
@@ -0,0 +1,11 @@
+# The letter case of these variables might seem odd. However:
+# - it is consistent with the serial_link example that is used as a reference in the Unit Testing article (https://docs.qmk.fm/#/unit_testing?id=adding-tests-for-new-or-existing-features)
+# - Neither `make test:sequencer` or `make test:SEQUENCER` work when using SCREAMING_SNAKE_CASE
+
+sequencer_DEFS := -DNO_DEBUG -DMIDI_MOCKED
+
+sequencer_SRC := \
+ $(QUANTUM_PATH)/sequencer/tests/midi_mock.c \
+ $(QUANTUM_PATH)/sequencer/tests/sequencer_tests.cpp \
+ $(QUANTUM_PATH)/sequencer/sequencer.c \
+ $(TMK_PATH)/common/test/timer.c
diff --git a/quantum/sequencer/tests/sequencer_tests.cpp b/quantum/sequencer/tests/sequencer_tests.cpp
new file mode 100644
index 0000000000..e81984e5b5
--- /dev/null
+++ b/quantum/sequencer/tests/sequencer_tests.cpp
@@ -0,0 +1,590 @@
+/* Copyright 2020 Rodolphe Belouin
+ *
+ * This program 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "gtest/gtest.h"
+
+extern "C" {
+#include "sequencer.h"
+#include "midi_mock.h"
+#include "quantum/quantum_keycodes.h"
+}
+
+extern "C" {
+void set_time(uint32_t t);
+void advance_time(uint32_t ms);
+}
+
+class SequencerTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ config_copy.enabled = sequencer_config.enabled;
+
+ for (int i = 0; i < SEQUENCER_STEPS; i++) {
+ config_copy.steps[i] = sequencer_config.steps[i];
+ }
+
+ for (int i = 0; i < SEQUENCER_TRACKS; i++) {
+ config_copy.track_notes[i] = sequencer_config.track_notes[i];
+ }
+
+ config_copy.tempo = sequencer_config.tempo;
+ config_copy.resolution = sequencer_config.resolution;
+
+ state_copy.active_tracks = sequencer_internal_state.active_tracks;
+ state_copy.current_track = sequencer_internal_state.current_track;
+ state_copy.current_step = sequencer_internal_state.current_step;
+ state_copy.timer = sequencer_internal_state.timer;
+
+ last_noteon = 0;
+ last_noteoff = 0;
+
+ set_time(0);
+ }
+
+ void TearDown() override {
+ sequencer_config.enabled = config_copy.enabled;
+
+ for (int i = 0; i < SEQUENCER_STEPS; i++) {
+ sequencer_config.steps[i] = config_copy.steps[i];
+ }
+
+ for (int i = 0; i < SEQUENCER_TRACKS; i++) {
+ sequencer_config.track_notes[i] = config_copy.track_notes[i];
+ }
+
+ sequencer_config.tempo = config_copy.tempo;
+ sequencer_config.resolution = config_copy.resolution;
+
+ sequencer_internal_state.active_tracks = state_copy.active_tracks;
+ sequencer_internal_state.current_track = state_copy.current_track;
+ sequencer_internal_state.current_step = state_copy.current_step;
+ sequencer_internal_state.timer = state_copy.timer;
+ }
+
+ sequencer_config_t config_copy;
+ sequencer_state_t state_copy;
+};
+
+TEST_F(SequencerTest, TestOffByDefault) { EXPECT_EQ(is_sequencer_on(), false); }
+
+TEST_F(SequencerTest, TestOn) {
+ sequencer_config.enabled = false;
+
+ sequencer_on();
+ EXPECT_EQ(is_sequencer_on(), true);
+
+ // sequencer_on is idempotent
+ sequencer_on();
+ EXPECT_EQ(is_sequencer_on(), true);
+}
+
+TEST_F(SequencerTest, TestOff) {
+ sequencer_config.enabled = true;
+
+ sequencer_off();
+ EXPECT_EQ(is_sequencer_on(), false);
+
+ // sequencer_off is idempotent
+ sequencer_off();
+ EXPECT_EQ(is_sequencer_on(), false);
+}
+
+TEST_F(SequencerTest, TestToggle) {
+ sequencer_config.enabled = false;
+
+ sequencer_toggle();
+ EXPECT_EQ(is_sequencer_on(), true);
+
+ sequencer_toggle();
+ EXPECT_EQ(is_sequencer_on(), false);
+}
+
+TEST_F(SequencerTest, TestNoActiveTrackByDefault) {
+ for (int i = 0; i < SEQUENCER_TRACKS; i++) {
+ EXPECT_EQ(is_sequencer_track_active(i), false);
+ }
+}
+
+TEST_F(SequencerTest, TestGetActiveTracks) {
+ sequencer_internal_state.active_tracks = (1 << 7) + (1 << 6) + (1 << 3) + (1 << 1) + (1 << 0);
+
+ EXPECT_EQ(is_sequencer_track_active(0), true);
+ EXPECT_EQ(is_sequencer_track_active(1), true);
+ EXPECT_EQ(is_sequencer_track_active(2), false);
+ EXPECT_EQ(is_sequencer_track_active(3), true);
+ EXPECT_EQ(is_sequencer_track_active(4), false);
+ EXPECT_EQ(is_sequencer_track_active(5), false);
+ EXPECT_EQ(is_sequencer_track_active(6), true);
+ EXPECT_EQ(is_sequencer_track_active(7), true);
+}
+
+TEST_F(SequencerTest, TestGetActiveTracksOutOfBound) {
+ sequencer_set_track_activation(-1, true);
+ sequencer_set_track_activation(8, true);
+
+ EXPECT_EQ(is_sequencer_track_active(-1), false);
+ EXPECT_EQ(is_sequencer_track_active(8), false);
+}
+
+TEST_F(SequencerTest, TestToggleTrackActivation) {
+ sequencer_internal_state.active_tracks = (1 << 7) + (1 << 6) + (1 << 3) + (1 << 1) + (1 << 0);
+
+ sequencer_toggle_track_activation(6);
+
+ EXPECT_EQ(is_sequencer_track_active(0), true);
+ EXPECT_EQ(is_sequencer_track_active(1), true);
+ EXPECT_EQ(is_sequencer_track_active(2), false);
+ EXPECT_EQ(is_sequencer_track_active(3), true);
+ EXPECT_EQ(is_sequencer_track_active(4), false);
+ EXPECT_EQ(is_sequencer_track_active(5), false);
+ EXPECT_EQ(is_sequencer_track_active(6), false);
+ EXPECT_EQ(is_sequencer_track_active(7), true);
+}
+
+TEST_F(SequencerTest, TestToggleSingleTrackActivation) {
+ sequencer_internal_state.active_tracks = (1 << 7) + (1 << 6) + (1 << 3) + (1 << 1) + (1 << 0);
+
+ sequencer_toggle_single_active_track(2);
+
+ EXPECT_EQ(is_sequencer_track_active(0), false);
+ EXPECT_EQ(is_sequencer_track_active(1), false);
+ EXPECT_EQ(is_sequencer_track_active(2), true);
+ EXPECT_EQ(is_sequencer_track_active(3), false);
+ EXPECT_EQ(is_sequencer_track_active(4), false);
+ EXPECT_EQ(is_sequencer_track_active(5), false);
+ EXPECT_EQ(is_sequencer_track_active(6), false);
+ EXPECT_EQ(is_sequencer_track_active(7), false);
+}
+
+TEST_F(SequencerTest, TestStepOffByDefault) {
+ for (int i = 0; i < SEQUENCER_STEPS; i++) {
+ EXPECT_EQ(is_sequencer_step_on(i), false);
+ }
+}
+
+TEST_F(SequencerTest, TestIsStepOffWithNoActiveTracks) {
+ sequencer_config.steps[3] = 0xFF;
+ EXPECT_EQ(is_sequencer_step_on(3), false);
+}
+
+TEST_F(SequencerTest, TestIsStepOffWithGivenActiveTracks) {
+ sequencer_set_track_activation(2, true);
+ sequencer_set_track_activation(3, true);
+
+ sequencer_config.steps[3] = (1 << 0) + (1 << 1);
+
+ // No active tracks have the step enabled, so it is off
+ EXPECT_EQ(is_sequencer_step_on(3), false);
+}
+
+TEST_F(SequencerTest, TestIsStepOnWithGivenActiveTracks) {
+ sequencer_set_track_activation(2, true);
+ sequencer_set_track_activation(3, true);
+
+ sequencer_config.steps[3] = (1 << 2);
+
+ // Track 2 has the step enabled, so it is on
+ EXPECT_EQ(is_sequencer_step_on(3), true);
+}
+
+TEST_F(SequencerTest, TestIsStepOffForGivenTrack) {
+ sequencer_config.steps[3] = 0x00;
+ EXPECT_EQ(is_sequencer_step_on_for_track(3, 5), false);
+}
+
+TEST_F(SequencerTest, TestIsStepOnForGivenTrack) {
+ sequencer_config.steps[3] = (1 << 5);
+ EXPECT_EQ(is_sequencer_step_on_for_track(3, 5), true);
+}
+
+TEST_F(SequencerTest, TestSetStepOn) {
+ sequencer_internal_state.active_tracks = (1 << 6) + (1 << 3) + (1 << 2);
+ sequencer_config.steps[2] = (1 << 5) + (1 << 2);
+
+ sequencer_set_step(2, true);
+
+ EXPECT_EQ(sequencer_config.steps[2], (1 << 6) + (1 << 5) + (1 << 3) + (1 << 2));
+}
+
+TEST_F(SequencerTest, TestSetStepOff) {
+ sequencer_internal_state.active_tracks = (1 << 6) + (1 << 3) + (1 << 2);
+ sequencer_config.steps[2] = (1 << 5) + (1 << 2);
+
+ sequencer_set_step(2, false);
+
+ EXPECT_EQ(sequencer_config.steps[2], (1 << 5));
+}
+
+TEST_F(SequencerTest, TestToggleStepOff) {
+ sequencer_internal_state.active_tracks = (1 << 6) + (1 << 3) + (1 << 2);
+ sequencer_config.steps[2] = (1 << 5) + (1 << 2);
+
+ sequencer_toggle_step(2);
+
+ EXPECT_EQ(sequencer_config.steps[2], (1 << 5));
+}
+
+TEST_F(SequencerTest, TestToggleStepOn) {
+ sequencer_internal_state.active_tracks = (1 << 6) + (1 << 3) + (1 << 2);
+ sequencer_config.steps[2] = 0;
+
+ sequencer_toggle_step(2);
+
+ EXPECT_EQ(sequencer_config.steps[2], (1 << 6) + (1 << 3) + (1 << 2));
+}
+
+TEST_F(SequencerTest, TestSetAllStepsOn) {
+ sequencer_internal_state.active_tracks = (1 << 6) + (1 << 3) + (1 << 2);
+ sequencer_config.steps[2] = (1 << 7) + (1 << 6);
+ sequencer_config.steps[4] = (1 << 3) + (1 << 1);
+
+ sequencer_set_all_steps(true);
+
+ EXPECT_EQ(sequencer_config.steps[2], (1 << 7) + (1 << 6) + (1 << 3) + (1 << 2));
+ EXPECT_EQ(sequencer_config.steps[4], (1 << 6) + (1 << 3) + (1 << 2) + (1 << 1));
+}
+
+TEST_F(SequencerTest, TestSetAllStepsOff) {
+ sequencer_internal_state.active_tracks = (1 << 6) + (1 << 3) + (1 << 2);
+ sequencer_config.steps[2] = (1 << 7) + (1 << 6);
+ sequencer_config.steps[4] = (1 << 3) + (1 << 1);
+
+ sequencer_set_all_steps(false);
+
+ EXPECT_EQ(sequencer_config.steps[2], (1 << 7));
+ EXPECT_EQ(sequencer_config.steps[4], (1 << 1));
+}
+
+TEST_F(SequencerTest, TestSetTempoZero) {
+ sequencer_config.tempo = 123;
+
+ sequencer_set_tempo(0);
+
+ EXPECT_EQ(sequencer_config.tempo, 123);
+}
+
+TEST_F(SequencerTest, TestIncreaseTempoMax) {
+ sequencer_config.tempo = UINT8_MAX;
+
+ sequencer_increase_tempo();
+
+ EXPECT_EQ(sequencer_config.tempo, UINT8_MAX);
+}
+
+TEST_F(SequencerTest, TestSetResolutionLowerBound) {
+ sequencer_config.resolution = SQ_RES_4;
+
+ sequencer_set_resolution((sequencer_resolution_t)-1);
+
+ EXPECT_EQ(sequencer_config.resolution, SQ_RES_4);
+}
+
+TEST_F(SequencerTest, TestSetResolutionUpperBound) {
+ sequencer_config.resolution = SQ_RES_4;
+
+ sequencer_set_resolution(SEQUENCER_RESOLUTIONS);
+
+ EXPECT_EQ(sequencer_config.resolution, SQ_RES_4);
+}
+
+TEST_F(SequencerTest, TestGetBeatDuration) {
+ EXPECT_EQ(get_beat_duration(60), 1000);
+ EXPECT_EQ(get_beat_duration(120), 500);
+ EXPECT_EQ(get_beat_duration(240), 250);
+ EXPECT_EQ(get_beat_duration(0), 1000);
+}
+
+TEST_F(SequencerTest, TestGetStepDuration60) {
+ /**
+ * Resolution cheatsheet:
+ * 1/2 => 2 steps per 4 beats
+ * 1/2T => 3 steps per 4 beats
+ * 1/4 => 4 steps per 4 beats
+ * 1/4T => 6 steps per 4 beats
+ * 1/8 => 8 steps per 4 beats
+ * 1/8T => 12 steps per 4 beats
+ * 1/16 => 16 steps per 4 beats
+ * 1/16T => 24 steps per 4 beats
+ * 1/32 => 32 steps per 4 beats
+ *
+ * The number of steps for binary resolutions follows the powers of 2.
+ * The ternary variants are simply 1.5x faster.
+ */
+ EXPECT_EQ(get_step_duration(60, SQ_RES_2), 2000);
+ EXPECT_EQ(get_step_duration(60, SQ_RES_4), 1000);
+ EXPECT_EQ(get_step_duration(60, SQ_RES_8), 500);
+ EXPECT_EQ(get_step_duration(60, SQ_RES_16), 250);
+ EXPECT_EQ(get_step_duration(60, SQ_RES_32), 125);
+
+ EXPECT_EQ(get_step_duration(60, SQ_RES_2T), 1333);
+ EXPECT_EQ(get_step_duration(60, SQ_RES_4T), 666);
+ EXPECT_EQ(get_step_duration(60, SQ_RES_8T), 333);
+ EXPECT_EQ(get_step_duration(60, SQ_RES_16T), 166);
+}
+
+TEST_F(SequencerTest, TestGetStepDuration120) {
+ /**
+ * Resolution cheatsheet:
+ * 1/2 => 2 steps per 4 beats
+ * 1/2T => 3 steps per 4 beats
+ * 1/4 => 4 steps per 4 beats
+ * 1/4T => 6 steps per 4 beats
+ * 1/8 => 8 steps per 4 beats
+ * 1/8T => 12 steps per 4 beats
+ * 1/16 => 16 steps per 4 beats
+ * 1/16T => 24 steps per 4 beats
+ * 1/32 => 32 steps per 4 beats
+ *
+ * The number of steps for binary resolutions follows the powers of 2.
+ * The ternary variants are simply 1.5x faster.
+ */
+ EXPECT_EQ(get_step_duration(30, SQ_RES_2), 4000);
+ EXPECT_EQ(get_step_duration(30, SQ_RES_4), 2000);
+ EXPECT_EQ(get_step_duration(30, SQ_RES_8), 1000);
+ EXPECT_EQ(get_step_duration(30, SQ_RES_16), 500);
+ EXPECT_EQ(get_step_duration(30, SQ_RES_32), 250);
+
+ EXPECT_EQ(get_step_duration(30, SQ_RES_2T), 2666);
+ EXPECT_EQ(get_step_duration(30, SQ_RES_4T), 1333);
+ EXPECT_EQ(get_step_duration(30, SQ_RES_8T), 666);
+ EXPECT_EQ(get_step_duration(30, SQ_RES_16T), 333);
+}
+
+void setUpMatrixScanSequencerTest(void) {
+ sequencer_config.enabled = true;
+ sequencer_config.tempo = 120;
+ sequencer_config.resolution = SQ_RES_16;
+
+ // Configure the notes for each track
+ sequencer_config.track_notes[0] = MI_C;
+ sequencer_config.track_notes[1] = MI_D;
+ sequencer_config.track_notes[2] = MI_E;
+ sequencer_config.track_notes[3] = MI_F;
+ sequencer_config.track_notes[4] = MI_G;
+ sequencer_config.track_notes[5] = MI_A;
+ sequencer_config.track_notes[6] = MI_B;
+ sequencer_config.track_notes[7] = MI_C;
+
+ // Turn on some steps
+ sequencer_config.steps[0] = (1 << 0);
+ sequencer_config.steps[2] = (1 << 1) + (1 << 0);
+}
+
+TEST_F(SequencerTest, TestMatrixScanSequencerShouldAttackFirstTrackOfFirstStep) {
+ setUpMatrixScanSequencerTest();
+
+ matrix_scan_sequencer();
+ EXPECT_EQ(last_noteon, MI_C);
+ EXPECT_EQ(last_noteoff, 0);
+}
+
+TEST_F(SequencerTest, TestMatrixScanSequencerShouldAttackSecondTrackAfterFirstTrackOfFirstStep) {
+ setUpMatrixScanSequencerTest();
+
+ matrix_scan_sequencer();
+ EXPECT_EQ(sequencer_internal_state.current_step, 0);
+ EXPECT_EQ(sequencer_internal_state.current_track, 1);
+ EXPECT_EQ(sequencer_internal_state.phase, SEQUENCER_PHASE_ATTACK);
+}
+
+TEST_F(SequencerTest, TestMatrixScanSequencerShouldNotAttackInactiveTrackFirstStep) {
+ setUpMatrixScanSequencerTest();
+
+ sequencer_internal_state.current_step = 0;
+ sequencer_internal_state.current_track = 1;
+
+ // Wait some time after the first track has been attacked
+ advance_time(SEQUENCER_TRACK_THROTTLE);
+
+ matrix_scan_sequencer();
+ EXPECT_EQ(last_noteon, 0);
+ EXPECT_EQ(last_noteoff, 0);
+}
+
+TEST_F(SequencerTest, TestMatrixScanSequencerShouldAttackThirdTrackAfterSecondTrackOfFirstStep) {
+ setUpMatrixScanSequencerTest();
+
+ sequencer_internal_state.current_step = 0;
+ sequencer_internal_state.current_track = 1;
+
+ // Wait some time after the second track has been attacked
+ advance_time(2 * SEQUENCER_TRACK_THROTTLE);
+
+ matrix_scan_sequencer();
+ EXPECT_EQ(sequencer_internal_state.current_step, 0);
+ EXPECT_EQ(sequencer_internal_state.current_track, 2);
+ EXPECT_EQ(sequencer_internal_state.phase, SEQUENCER_PHASE_ATTACK);
+}
+
+TEST_F(SequencerTest, TestMatrixScanSequencerShouldEnterReleasePhaseAfterLastTrackHasBeenProcessedFirstStep) {
+ setUpMatrixScanSequencerTest();
+
+ sequencer_internal_state.current_step = 0;
+ sequencer_internal_state.current_track = SEQUENCER_TRACKS - 1;
+
+ // Wait until all notes have been attacked
+ advance_time((SEQUENCER_TRACKS - 1) * SEQUENCER_TRACK_THROTTLE);
+
+ matrix_scan_sequencer();
+ EXPECT_EQ(last_noteon, 0);
+ EXPECT_EQ(last_noteoff, 0);
+ EXPECT_EQ(sequencer_internal_state.current_step, 0);
+ EXPECT_EQ(sequencer_internal_state.current_track, SEQUENCER_TRACKS - 1);
+ EXPECT_EQ(sequencer_internal_state.phase, SEQUENCER_PHASE_RELEASE);
+}
+
+TEST_F(SequencerTest, TestMatrixScanSequencerShouldReleaseBackwards) {
+ setUpMatrixScanSequencerTest();
+
+ sequencer_internal_state.current_step = 0;
+ sequencer_internal_state.current_track = SEQUENCER_TRACKS - 1;
+ sequencer_internal_state.phase = SEQUENCER_PHASE_RELEASE;
+
+ // Wait until all notes have been attacked
+ advance_time((SEQUENCER_TRACKS - 1) * SEQUENCER_TRACK_THROTTLE);
+ // + the release timeout
+ advance_time(SEQUENCER_PHASE_RELEASE_TIMEOUT);
+
+ matrix_scan_sequencer();
+ EXPECT_EQ(sequencer_internal_state.current_step, 0);
+ EXPECT_EQ(sequencer_internal_state.current_track, SEQUENCER_TRACKS - 2);
+ EXPECT_EQ(sequencer_internal_state.phase, SEQUENCER_PHASE_RELEASE);
+}
+
+TEST_F(SequencerTest, TestMatrixScanSequencerShouldNotReleaseInactiveTrackFirstStep) {
+ setUpMatrixScanSequencerTest();
+
+ sequencer_internal_state.current_step = 0;
+ sequencer_internal_state.current_track = SEQUENCER_TRACKS - 1;
+ sequencer_internal_state.phase = SEQUENCER_PHASE_RELEASE;
+
+ // Wait until all notes have been attacked
+ advance_time((SEQUENCER_TRACKS - 1) * SEQUENCER_TRACK_THROTTLE);
+ // + the release timeout
+ advance_time(SEQUENCER_PHASE_RELEASE_TIMEOUT);
+
+ matrix_scan_sequencer();
+ EXPECT_EQ(last_noteon, 0);
+ EXPECT_EQ(last_noteoff, 0);
+}
+
+TEST_F(SequencerTest, TestMatrixScanSequencerShouldReleaseFirstTrackFirstStep) {
+ setUpMatrixScanSequencerTest();
+
+ sequencer_internal_state.current_step = 0;
+ sequencer_internal_state.current_track = 0;
+ sequencer_internal_state.phase = SEQUENCER_PHASE_RELEASE;
+
+ // Wait until all notes have been attacked
+ advance_time((SEQUENCER_TRACKS - 1) * SEQUENCER_TRACK_THROTTLE);
+ // + the release timeout
+ advance_time(SEQUENCER_PHASE_RELEASE_TIMEOUT);
+ // + all the other notes have been released
+ advance_time((SEQUENCER_TRACKS - 1) * SEQUENCER_TRACK_THROTTLE);
+
+ matrix_scan_sequencer();
+ EXPECT_EQ(last_noteon, 0);
+ EXPECT_EQ(last_noteoff, MI_C);
+}
+
+TEST_F(SequencerTest, TestMatrixScanSequencerShouldEnterPausePhaseAfterRelease) {
+ setUpMatrixScanSequencerTest();
+
+ sequencer_internal_state.current_step = 0;
+ sequencer_internal_state.current_track = 0;
+ sequencer_internal_state.phase = SEQUENCER_PHASE_RELEASE;
+
+ // Wait until all notes have been attacked
+ advance_time((SEQUENCER_TRACKS - 1) * SEQUENCER_TRACK_THROTTLE);
+ // + the release timeout
+ advance_time(SEQUENCER_PHASE_RELEASE_TIMEOUT);
+ // + all the other notes have been released
+ advance_time((SEQUENCER_TRACKS - 1) * SEQUENCER_TRACK_THROTTLE);
+
+ matrix_scan_sequencer();
+ EXPECT_EQ(sequencer_internal_state.current_step, 0);
+ EXPECT_EQ(sequencer_internal_state.current_track, 0);
+ EXPECT_EQ(sequencer_internal_state.phase, SEQUENCER_PHASE_PAUSE);
+}
+
+TEST_F(SequencerTest, TestMatrixScanSequencerShouldProcessFirstTrackOfSecondStepAfterPause) {
+ setUpMatrixScanSequencerTest();
+
+ sequencer_internal_state.current_step = 0;
+ sequencer_internal_state.current_track = 0;
+ sequencer_internal_state.phase = SEQUENCER_PHASE_PAUSE;
+
+ // Wait until all notes have been attacked
+ advance_time((SEQUENCER_TRACKS - 1) * SEQUENCER_TRACK_THROTTLE);
+ // + the release timeout
+ advance_time(SEQUENCER_PHASE_RELEASE_TIMEOUT);
+ // + all the other notes have been released
+ advance_time((SEQUENCER_TRACKS - 1) * SEQUENCER_TRACK_THROTTLE);
+ // + the step duration (one 16th at tempo=120 lasts 125ms)
+ advance_time(125);
+
+ matrix_scan_sequencer();
+ EXPECT_EQ(sequencer_internal_state.current_step, 1);
+ EXPECT_EQ(sequencer_internal_state.current_track, 1);
+ EXPECT_EQ(sequencer_internal_state.phase, SEQUENCER_PHASE_ATTACK);
+}
+
+TEST_F(SequencerTest, TestMatrixScanSequencerShouldProcessSecondTrackTooEarly) {
+ setUpMatrixScanSequencerTest();
+
+ sequencer_internal_state.current_step = 2;
+ sequencer_internal_state.current_track = 1;
+
+ matrix_scan_sequencer();
+ EXPECT_EQ(last_noteon, 0);
+ EXPECT_EQ(last_noteoff, 0);
+}
+
+TEST_F(SequencerTest, TestMatrixScanSequencerShouldProcessSecondTrackOnTime) {
+ setUpMatrixScanSequencerTest();
+
+ sequencer_internal_state.current_step = 2;
+ sequencer_internal_state.current_track = 1;
+
+ // Wait until first track has been attacked
+ advance_time(SEQUENCER_TRACK_THROTTLE);
+
+ matrix_scan_sequencer();
+ EXPECT_EQ(last_noteon, MI_D);
+ EXPECT_EQ(last_noteoff, 0);
+}
+
+TEST_F(SequencerTest, TestMatrixScanSequencerShouldLoopOnceSequenceIsOver) {
+ setUpMatrixScanSequencerTest();
+
+ sequencer_internal_state.current_step = SEQUENCER_STEPS - 1;
+ sequencer_internal_state.current_track = 0;
+ sequencer_internal_state.phase = SEQUENCER_PHASE_PAUSE;
+
+ // Wait until all notes have been attacked
+ advance_time((SEQUENCER_TRACKS - 1) * SEQUENCER_TRACK_THROTTLE);
+ // + the release timeout
+ advance_time(SEQUENCER_PHASE_RELEASE_TIMEOUT);
+ // + all the other notes have been released
+ advance_time((SEQUENCER_TRACKS - 1) * SEQUENCER_TRACK_THROTTLE);
+ // + the step duration (one 16th at tempo=120 lasts 125ms)
+ advance_time(125);
+
+ matrix_scan_sequencer();
+ EXPECT_EQ(sequencer_internal_state.current_step, 0);
+ EXPECT_EQ(sequencer_internal_state.current_track, 1);
+ EXPECT_EQ(sequencer_internal_state.phase, SEQUENCER_PHASE_ATTACK);
+}
diff --git a/quantum/sequencer/tests/testlist.mk b/quantum/sequencer/tests/testlist.mk
new file mode 100644
index 0000000000..bb38991109
--- /dev/null
+++ b/quantum/sequencer/tests/testlist.mk
@@ -0,0 +1 @@
+TEST_LIST += sequencer
diff --git a/quantum/split_common/matrix.c b/quantum/split_common/matrix.c
index 5bad9db08f..cd5a024c3d 100644
--- a/quantum/split_common/matrix.c
+++ b/quantum/split_common/matrix.c
@@ -45,6 +45,19 @@ uint8_t thisHand, thatHand;
// user-defined overridable functions
__attribute__((weak)) void matrix_slave_scan_user(void) {}
+static inline void setPinOutput_writeLow(pin_t pin) {
+ ATOMIC_BLOCK_FORCEON {
+ setPinOutput(pin);
+ writePinLow(pin);
+ }
+}
+
+static inline void setPinInputHigh_atomic(pin_t pin) {
+ ATOMIC_BLOCK_FORCEON {
+ setPinInputHigh(pin);
+ }
+}
+
// matrix code
#ifdef DIRECT_PINS
@@ -83,22 +96,23 @@ static bool read_cols_on_row(matrix_row_t current_matrix[], uint8_t current_row)
# if (DIODE_DIRECTION == COL2ROW)
static void select_row(uint8_t row) {
- setPinOutput(row_pins[row]);
- writePinLow(row_pins[row]);
+ setPinOutput_writeLow(row_pins[row]);
}
-static void unselect_row(uint8_t row) { setPinInputHigh(row_pins[row]); }
+static void unselect_row(uint8_t row) {
+ setPinInputHigh_atomic(row_pins[row]);
+}
static void unselect_rows(void) {
for (uint8_t x = 0; x < ROWS_PER_HAND; x++) {
- setPinInputHigh(row_pins[x]);
+ setPinInputHigh_atomic(row_pins[x]);
}
}
static void init_pins(void) {
unselect_rows();
for (uint8_t x = 0; x < MATRIX_COLS; x++) {
- setPinInputHigh(col_pins[x]);
+ setPinInputHigh_atomic(col_pins[x]);
}
}
@@ -133,22 +147,23 @@ static bool read_cols_on_row(matrix_row_t current_matrix[], uint8_t current_row)
# elif (DIODE_DIRECTION == ROW2COL)
static void select_col(uint8_t col) {
- setPinOutput(col_pins[col]);
- writePinLow(col_pins[col]);
+ setPinOutput_writeLow(col_pins[col]);
}
-static void unselect_col(uint8_t col) { setPinInputHigh(col_pins[col]); }
+static void unselect_col(uint8_t col) {
+ setPinInputHigh_atomic(col_pins[col]);
+}
static void unselect_cols(void) {
for (uint8_t x = 0; x < MATRIX_COLS; x++) {
- setPinInputHigh(col_pins[x]);
+ setPinInputHigh_atomic(col_pins[x]);
}
}
static void init_pins(void) {
unselect_cols();
for (uint8_t x = 0; x < ROWS_PER_HAND; x++) {
- setPinInputHigh(row_pins[x]);
+ setPinInputHigh_atomic(row_pins[x]);
}
}