diff --git a/build_test.mk b/build_test.mk index d13d9a515b..e705c6a3ba 100644 --- a/build_test.mk +++ b/build_test.mk @@ -49,6 +49,7 @@ endif include common_features.mk include $(TMK_PATH)/common.mk +include $(QUANTUM_PATH)/sequencer/tests/rules.mk include $(QUANTUM_PATH)/serial_link/tests/rules.mk ifneq ($(filter $(FULL_TESTS),$(TEST)),) include build_full_test.mk diff --git a/common.mk b/common.mk index aea29a7a20..c13b5e2768 100644 --- a/common.mk +++ b/common.mk @@ -21,4 +21,5 @@ COMMON_VPATH += $(QUANTUM_PATH)/keymap_extras COMMON_VPATH += $(QUANTUM_PATH)/audio COMMON_VPATH += $(QUANTUM_PATH)/process_keycode COMMON_VPATH += $(QUANTUM_PATH)/api +COMMON_VPATH += $(QUANTUM_PATH)/sequencer COMMON_VPATH += $(DRIVER_PATH) diff --git a/common_features.mk b/common_features.mk index 32028c8af5..b49c08c50b 100644 --- a/common_features.mk +++ b/common_features.mk @@ -38,6 +38,13 @@ ifeq ($(strip $(AUDIO_ENABLE)), yes) SRC += $(QUANTUM_DIR)/audio/luts.c endif +ifeq ($(strip $(SEQUENCER_ENABLE)), yes) + OPT_DEFS += -DSEQUENCER_ENABLE + MUSIC_ENABLE = yes + SRC += $(QUANTUM_DIR)/sequencer/sequencer.c + SRC += $(QUANTUM_DIR)/process_keycode/process_sequencer.c +endif + ifeq ($(strip $(MIDI_ENABLE)), yes) OPT_DEFS += -DMIDI_ENABLE MUSIC_ENABLE = yes diff --git a/docs/_summary.md b/docs/_summary.md index 2e874fb1cd..864fdee0db 100644 --- a/docs/_summary.md +++ b/docs/_summary.md @@ -80,6 +80,7 @@ * [One Shot Keys](one_shot_keys.md) * [Pointing Device](feature_pointing_device.md) * [Raw HID](feature_rawhid.md) + * [Sequencer](feature_sequencer.md) * [Swap Hands](feature_swap_hands.md) * [Tap Dance](feature_tap_dance.md) * [Tap-Hold Configuration](tap_hold.md) diff --git a/docs/feature_sequencer.md b/docs/feature_sequencer.md new file mode 100644 index 0000000000..8c8587a9b8 --- /dev/null +++ b/docs/feature_sequencer.md @@ -0,0 +1,88 @@ +# Sequencer + +Since QMK has experimental support for MIDI, you can now turn your keyboard into a [step sequencer](https://en.wikipedia.org/wiki/Music_sequencer#Step_sequencers)! + +!> **IMPORTANT:** This feature is highly experimental, it has only been tested on a Planck EZ so far. Also, the scope will be limited to support the drum machine use-case to start with. + +## Enable the step sequencer + +Add the following line to your `rules.mk`: + +```make +SEQUENCER_ENABLE = yes +``` + +By default the sequencer has 16 steps, but you can override this setting in your `config.h`: + +```c +#define SEQUENCER_STEPS 32 +``` + +## Tracks + +You can program up to 8 independent tracks with the step sequencer. Select the tracks you want to edit, enable or disable some steps, and start the sequence! + +## Resolutions + +While the tempo defines the absolute speed at which the sequencer goes through the steps, the resolution defines the granularity of these steps (from coarser to finer). + +|Resolution |Description | +|---------- |----------- | +|`SQ_RES_2` |Every other beat | +|`SQ_RES_2T` |Every 1.5 beats | +|`SQ_RES_4` |Every beat | +|`SQ_RES_4T` |Three times per 2 beats| +|`SQ_RES_8` |Twice per beat | +|`SQ_RES_8T` |Three times per beat | +|`SQ_RES_16` |Four times per beat | +|`SQ_RES_16T` |Six times per beat | +|`SQ_RES_32` |Eight times per beat | + +## Keycodes + +|Keycode |Description | +|------- |----------- | +|`SQ_ON` |Start the step sequencer | +|`SQ_OFF` |Stop the step sequencer | +|`SQ_TOG` |Toggle the step sequencer playback | +|`SQ_SALL`|Enable all the steps | +|`SQ_SCLR`|Disable all the steps | +|`SQ_S(n)`|Toggle the step `n` | +|`SQ_TMPD`|Decrease the tempo | +|`SQ_TMPU`|Increase the tempo | +|`SQ_R(n)`|Set the resolution to n | +|`SQ_RESD`|Change to the slower resolution | +|`SQ_RESU`|Change to the faster resolution | +|`SQ_T(n)`|Set `n` as the only active track or deactivate all | + +## Functions + +|Function |Description | +|-------- |----------- | +|`bool is_sequencer_on(void);` |Return whether the sequencer is playing | +|`void sequencer_toggle(void);` |Toggle the step sequencer playback | +|`void sequencer_on(void);` |Start the step sequencer | +|`void sequencer_off(void);` |Stop the step sequencer | +|`bool is_sequencer_step_on(uint8_t step);` |Return whether the step is currently enabled | +|`void sequencer_set_step(uint8_t step, bool value);` |Enable or disable the step | +|`void sequencer_set_step_on();` |Enable the step | +|`void sequencer_set_step_off();` |Disable the step | +|`void sequencer_toggle_step(uint8_t step);` |Toggle the step | +|`void sequencer_set_all_steps(bool value);` |Enable or disable all the steps | +|`void sequencer_set_all_steps_on();` |Enable all the steps | +|`void sequencer_set_all_steps_off();` |Disable all the steps | +|`uint8_t sequencer_get_tempo(void);` |Return the current tempo | +|`void sequencer_set_tempo(uint8_t tempo);` |Set the tempo to `tempo` (between 1 and 255) | +|`void sequencer_increase_tempo(void);` |Increase the tempo | +|`void sequencer_decrease_tempo(void);` |Decrease the tempo | +|`sequencer_resolution_t sequencer_get_resolution(void);` |Return the current resolution | +|`void sequencer_set_resolution(sequencer_resolution_t resolution);` |Set the resolution to `resolution` | +|`void sequencer_increase_resolution(void);` |Change to the faster resolution | +|`void sequencer_decrease_resolution(void);` |Change to the slower resolution | +|`bool is_sequencer_track_active(uint8_t track);` |Return whether the track is active | +|`void sequencer_set_track_activation(uint8_t track, bool value);` |Activate or deactivate the `track` | +|`void sequencer_toggle_track_activation(uint8_t track);` |Toggle the `track` | +|`void sequencer_activate_track(uint8_t track);` |Activate the `track` | +|`void sequencer_deactivate_track(uint8_t track);` |Deactivate the `track` | +|`void sequencer_toggle_single_active_track(uint8_t track);` |Set `track` as the only active track or deactivate all | + 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 . + */ + +#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 . + */ + +#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 dab6c9172f..ebe80ca1ea 100644 --- a/quantum/quantum.c +++ b/quantum/quantum.c @@ -229,6 +229,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 @@ -633,6 +636,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 diff --git a/quantum/quantum.h b/quantum/quantum.h index 42e8c00091..e63711fc31 100644 --- a/quantum/quantum.h +++ b/quantum/quantum.h @@ -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 diff --git a/quantum/quantum_keycodes.h b/quantum/quantum_keycodes.h index c1e31fc867..a2cc7b38d9 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 @@ -550,6 +554,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/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 . + */ + +#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 . + */ + +#pragma once + +#include +#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 . + */ + +#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 . + */ + +#pragma once + +#include + +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 . + */ + +#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/testlist.mk b/testlist.mk index d949fb3eae..ca1408806c 100644 --- a/testlist.mk +++ b/testlist.mk @@ -1,6 +1,7 @@ TEST_LIST = $(notdir $(patsubst %/rules.mk,%,$(wildcard $(ROOT_DIR)/tests/*/rules.mk))) FULL_TESTS := $(TEST_LIST) +include $(ROOT_DIR)/quantum/sequencer/tests/testlist.mk include $(ROOT_DIR)/quantum/serial_link/tests/testlist.mk define VALIDATE_TEST_LIST