From d5ca05edff4b06f41c05dbbb7ad7d943e6d624ea Mon Sep 17 00:00:00 2001 From: David Robillard Date: Sat, 19 Jun 2021 00:52:29 -0400 Subject: [PATCH] Push2: Implement a vertical interval for "tuning" the pads The implementation is generic, but the actual call is hardcoded to 4ths here. UI to follow. --- libs/surfaces/push2/push2.cc | 265 +++++++++++++++++++++-------------- libs/surfaces/push2/push2.h | 47 +++++++ 2 files changed, 210 insertions(+), 102 deletions(-) diff --git a/libs/surfaces/push2/push2.cc b/libs/surfaces/push2/push2.cc index 3ffca29c6c..4047459e4c 100644 --- a/libs/surfaces/push2/push2.cc +++ b/libs/surfaces/push2/push2.cc @@ -17,6 +17,9 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +#include +#include + #include #include @@ -1330,126 +1333,184 @@ Push2::set_pad_note_kind (Pad& pad, const PadNoteKind kind) write (pad.state_msg ()); } -void -Push2::set_pad_scale (int root, int octave, MusicalMode::Type mode, bool inkey) +/** Return a bitset of notes in a musical mode. + * + * The returned bitset has a bit for every possible MIDI note number, which is + * set if the note is in the mode in any octave. + */ +static std::bitset<128> +mode_notes_bitset (const int scale_root, + const int octave, + const MusicalMode::Type mode) { - MusicalMode m (mode); - std::vector::iterator interval; - int note; - const int original_root = root; + std::bitset<128> notes_bitset; - interval = m.steps.begin(); - root += (octave*12); - note = root; + const std::vector mode_steps = MusicalMode (mode).steps; + int root = scale_root - 12; - const int root_start = root; - - std::set mode_map; /* contains only notes in mode, O(logN) lookup */ - std::vector mode_vector; /* sorted in note order */ - - mode_map.insert (note); - mode_vector.push_back (note); - - /* build a map of all notes in the mode, from the root to 127 */ - - while (note < 128) { - - if (interval == m.steps.end()) { - - /* last distance was the end of the scale, - so wrap, adding the next note at one - octave above the last root. - */ - - interval = m.steps.begin(); + // Repeatedly loop through the intervals in an octave + for (std::vector::const_iterator i = mode_steps.begin ();;) { + if (i == mode_steps.end ()) { + // Reached the end of the scale, continue with the next octave root += 12; - mode_map.insert (root); - mode_vector.push_back (root); + if (root > 127) { + break; + } + + notes_bitset.set (root); + i = mode_steps.begin (); } else { - note = (int) floor (root + (2.0 * (*interval))); - interval++; - mode_map.insert (note); - mode_vector.push_back (note); + const int note = (int)floor (root + (2.0 * (*i))); + if (note > 127) { + break; + } + + if (note > 0) { + notes_bitset.set (note); + } + + ++i; } } + return notes_bitset; +} + +/** Return a sorted vector of all notes in a musical mode. + * + * The returned vector has every possible MIDI note number (0 through 127 + * inclusive) that is in the mode in any octave. + */ +static std::vector +mode_notes_vector (const int scale_root, + const int octave, + const MusicalMode::Type mode) +{ + std::vector notes_vector; + + const std::vector mode_steps = MusicalMode (mode).steps; + int root = scale_root - 12; + + // Repeatedly loop through the intervals in an octave + for (std::vector::const_iterator i = mode_steps.begin ();;) { + if (i == mode_steps.end ()) { + // Reached the end of the scale, continue with the next octave + root += 12; + if (root > 127) { + break; + } + + notes_vector.push_back (root); + i = mode_steps.begin (); + + } else { + const int note = (int)floor (root + (2.0 * (*i))); + if (note > 127) { + break; + } + + if (note > 0) { + notes_vector.push_back (note); + } + + ++i; + } + } + + return notes_vector; +} + +void +Push2::set_pad_scale_in_key (const int scale_root, + const int octave, + const MusicalMode::Type mode, + const int ideal_vertical_semitones) +{ + const std::vector notes = mode_notes_vector (scale_root, octave, mode); + + for (int row = 0; row < 8; ++row) { + // The ideal leftmost note in a row is based only on the "tuning" + const int ideal_leftmost_note = 36 + (ideal_vertical_semitones * row); + + // If that's in the scale, use it, otherwise use the closest higher note + std::vector::const_iterator n = + std::lower_bound (notes.begin (), notes.end (), ideal_leftmost_note); + + // Set up the the following columns in the row using the scale + for (int col = 0; col < 8 && n != notes.end (); ++col) { + const int note = *n++; + const int index = 36 + (row * 8) + col; + const boost::shared_ptr& pad = _nn_pad_map[index]; + + pad->filtered = note; // Generated note number + + _fn_pad_map.insert (std::make_pair (note, pad)); + + if ((note % 12) == scale_root) { + set_pad_note_kind (*pad, RootNote); + } else { + set_pad_note_kind (*pad, InScaleNote); + } + } + } +} + +void +Push2::set_pad_scale_chromatic (const int scale_root, + const int octave, + const MusicalMode::Type mode, + const int vertical_semitones) +{ + const std::bitset<128> notes = mode_notes_bitset (scale_root, octave, mode); + + for (int row = 0; row < 8; ++row) { + // The leftmost note in a row is just based only on the "tuning" + const int leftmost_note = 36 + (vertical_semitones * row); + + // Set up the the following columns in the row using the scale + for (int col = 0; col < 8; ++col) { + const int note = leftmost_note + col; + const int index = 36 + (row * 8) + col; + const boost::shared_ptr& pad = _nn_pad_map[index]; + + pad->filtered = note; // Generated note number + + _fn_pad_map.insert (std::make_pair (note, pad)); + + if (!notes.test (note)) { + set_pad_note_kind (*pad, OutOfScaleNote); + } else if ((note % 12) == scale_root) { + set_pad_note_kind (*pad, RootNote); + } else { + set_pad_note_kind (*pad, InScaleNote); + } + } + } +} + +void +Push2::set_pad_scale (const int scale_root, + const int octave, + const MusicalMode::Type mode, + const bool inkey) +{ + // Clear the pad map and call the appropriate method to set them up again + _fn_pad_map.clear (); if (inkey) { - - std::vector::iterator notei; - int row_offset = 0; - - for (int row = 0; row < 8; ++row) { - - /* Ableton's grid layout wraps the available notes in the scale - * by offsetting 3 notes per row (from the bottom) - */ - - notei = mode_vector.begin(); - notei += row_offset; - row_offset += 3; - - for (int col = 0; col < 8; ++col) { - int index = 36 + (row*8) + col; - boost::shared_ptr pad = _nn_pad_map[index]; - int notenum; - if (notei != mode_vector.end()) { - - notenum = *notei; - pad->filtered = notenum; - - _fn_pad_map.insert (make_pair (notenum, pad)); - - if ((notenum % 12) == original_root) { - set_pad_note_kind(*pad, RootNote); - } else { - set_pad_note_kind(*pad, InScaleNote); - } - - notei++; - - } else { - pad->filtered = -1; - set_pad_note_kind(*pad, OutOfScaleNote); - } - } - } - + set_pad_scale_in_key(scale_root, octave, mode, 5); } else { - - /* chromatic: all notes available, but highlight those in the scale */ - - for (note = 36; note < 100; ++note) { - - boost::shared_ptr pad = _nn_pad_map[note]; - - /* Chromatic: all pads play, half-tone steps. Light - * those in the scale, and highlight root notes - */ - - pad->filtered = root_start + (note - 36); - - _fn_pad_map.insert (make_pair (pad->filtered, pad)); - - if (mode_map.find (note) == mode_map.end()) { - pad->filtered = -1; - set_pad_note_kind(*pad, OutOfScaleNote); - } else if ((note % 12) == original_root) { - set_pad_note_kind(*pad, RootNote); - } else { - set_pad_note_kind(*pad, InScaleNote); - } - } + set_pad_scale_chromatic(scale_root, octave, mode, 5); } - /* store state */ + // Store state bool changed = false; - if (_scale_root != original_root) { - _scale_root = original_root; + if (_scale_root != scale_root) { + _scale_root = scale_root; changed = true; } if (_root_octave != octave) { diff --git a/libs/surfaces/push2/push2.h b/libs/surfaces/push2/push2.h index 510da3fd8d..e512dd5aaf 100644 --- a/libs/surfaces/push2/push2.h +++ b/libs/surfaces/push2/push2.h @@ -331,6 +331,53 @@ class Push2 : public ARDOUR::ControlProtocol /// Set up a pad to represent a "kind" of note void set_pad_note_kind(Pad& pad, PadNoteKind kind); + /** Set an "in-key" scale on the pads. + * + * "In-key" mode shows only notes which are in the scale, so every pad is + * in the scale and there are no "spaces". This provides access to a wide + * range of notes in the scale, but no access to notes outside the scale at + * all. + * + * + * @param root The root note in the lowest octave (at most 11). + * + * @param octave The octave number of the "actual" root (at most 10). + * + * @param mode The active musical mode (scale). + * + * @param ideal_vertical_semitones The ideal interval between rows in + * semitones. This is an "ideal" because it may not be possible to use + * exactly this interval for every row depending on the scale. It may be + * bumped up to the next note in the scale if necessary, so with this mode, + * rows are not guaranteed to all have the same vertical interval. + */ + void set_pad_scale_in_key (int root, + int octave, + MusicalMode::Type mode, + int ideal_vertical_semitones); + + /** Set a "chromatic" scale on the pads. + * + * "Chromatic" mode is chromatic from left to right, and "tuned" to some + * interval from bottom up, like a stringed instrument. The default + * vertical interval is 5 semitones, or a perfect 4th, like strings on a + * bass guitar. + * + * @param root The root note in the lowest octave (at most 11). + * + * @param octave The octave number of the "actual" root (at most 10). + * + * @param mode The active musical mode (scale). + * + * @param vertical_semitones The interval between rows in semitones. This + * mode guarantees that the vertical interval for all rows is always + * exactly this. + */ + void set_pad_scale_chromatic (int root, + int octave, + MusicalMode::Type mode, + int vertical_semitones); + void set_pad_scale (int root, int octave, MusicalMode::Type mode, bool inkey); PBD::Signal0 ScaleChange;