Arpy.h
  1  // SPDX-FileCopyrightText: 2022 Tod Kurt for Adafruit Industries
  2  //
  3  // SPDX-License-Identifier: MIT
  4  
  5  /**
  6   * Arpy - a class to do arpeggios for you
  7   * 12 Jan 2022 - Tod Kurt @todbot
  8   * See https://github.com/todbot/mozzi_experiments/ for examples
  9   */
 10  class Arpy 
 11  {
 12    public:
 13    
 14    Arpy(): enabled(false), arp_id(0), arp_pos(0), root_note(0), note_played(0)
 15    {
 16      tr_steps = 1; tr_dist = 12;  tr_pos = 0; // FIXME make these defaults
 17    }
 18  
 19    // turn Arpy on or off
 20    void on() { enabled = true; }
 21    void off() {
 22        enabled = false;
 23        if( noteOffHandler) { noteOffHandler( note_played ); }
 24    }
 25    // let you know what Arpy thinks about doing its job
 26    bool isOn() { return enabled; }
 27  
 28    // pick which arpeggio to play
 29    void setArpId(uint8_t arp_id) { arp_id = arp_id % arp_count; }
 30  
 31    // which arpeggio is currently set
 32    uint8_t getArpId() { return arp_id; }
 33  
 34    // go to next arpeggio, a little convenience function
 35    void nextArpId() {  arp_id = (arp_id+1) % arp_count; }
 36  
 37    // return acount of how many different arpeggios Arpy knows
 38    uint8_t getArpCount() { return arp_count; }
 39  
 40    // set the root note of the arpeggio
 41    void setRootNote( uint8_t note ) { root_note = note; }
 42    uint8_t getRootNote() { return root_note; }
 43  
 44    // set the octave offset for root_note (root_note += (octave_offset*12)
 45    void setOctaveOffset( uint8_t offset ) { octave_offset = offset; }
 46    uint8_t getOctaveOffset() { return octave_offset; }
 47  
 48    // set BPM of arpeggio
 49    void setBPM( float bpm ) {
 50      bpm = bpm;
 51      per_beat_millis = 1000 * 60 / bpm;
 52      note_duration = gate_percentage * per_beat_millis;
 53    }
 54  
 55    // set note duration as a percentage of BPM
 56    void setGateTime( float percentage ) {
 57      if( percentage < 0 || percentage > 1 ) { return; }
 58      gate_percentage = percentage;
 59    }
 60    float getGateTime() { return gate_percentage; } // percentage 0.0 - 1.0
 61  
 62    // set the function to call when note on happens
 63    void setNoteOnHandler(void (*anoteOnHandler)(uint8_t)) {
 64      noteOnHandler = anoteOnHandler;
 65    }
 66  
 67    // set the function to call when note off happens
 68    void setNoteOffHandler(void (*anoteOffHandler)(uint8_t)) {
 69      noteOffHandler = anoteOffHandler;
 70    }
 71  
 72    // number of "octaves" (if transpose distance=12)
 73    void setTransposeSteps(uint8_t steps) {  if( steps > 0) { tr_steps = steps; } }
 74  
 75    // the distance in semitones between steps, often 12 for an octave
 76    void setTransposeDistance(uint8_t dist) {  tr_dist = dist; }
 77  
 78    // call update as fast as possible, will trigger noteOn and noteOff functions
 79    void update() { update(-1); } // -1 means "root note is updated immediately when changed"
 80  
 81    // call update as fast as possible, will trigger noteOn and noteOff function
 82    // "root_note_new" is new root note to use at top of arp "measure"
 83    // negative notes are invalid. "root_note_new == -1" is used as internal signaling
 84    void update(int root_note_new)
 85    {
 86      if( !enabled ) { return; }
 87  
 88      uint32_t now = millis();
 89  
 90      if( now - last_beat_millis > per_beat_millis )  {
 91        last_beat_millis = now;
 92        int8_t tr_amount = tr_dist * tr_pos;  // tr_pos may be zero
 93        
 94        // only make musical changes at start of a new measure // FIXME allow immediate
 95        if( arp_pos == 0 ) {
 96          if( root_note_new >= 0 ) { root_note = root_note_new; }
 97          tr_pos = (tr_pos + 1) % tr_steps;
 98        }
 99        note_played = root_note + arps[arp_id][arp_pos] + tr_amount;
100        note_played = constrain(note_played + (12 * octave_offset), 0,127);
101        if( noteOnHandler) { noteOnHandler( note_played ); }
102        arp_pos = (arp_pos+1) % arp_len;
103      }
104    
105      if( now - last_beat_millis > note_duration ) { 
106        if( note_played != 0 ) { // we have a note to turn off!
107          if( noteOffHandler) { noteOffHandler( note_played ); }
108          note_played = 0;  // say we've turned off the note
109        }
110      }
111    }
112  
113    private:
114      bool    enabled;       // is arp playing or not
115      float   bpm;           // our arps per minute
116      uint8_t arp_id;        // which arp we using
117      uint8_t arp_pos;       // where in current arp are we
118      uint8_t tr_steps;      // if transposing, how many times, 1= normal
119      int8_t  tr_dist;       // like an octave
120      uint8_t tr_pos;        // which tranposed we're on (0..tr_steps)
121      uint8_t root_note;     //
122      uint8_t octave_offset; // offset for root_note
123      uint16_t per_beat_millis; // = 1000 * 60 / bpm;
124  
125      uint8_t note_played;    // the note we have played, so we can unplay it later
126      uint16_t note_duration;
127      float gate_percentage;  // percentage of per_beat_millis to play a note, 0.0-1.0
128  
129      uint32_t last_beat_millis;
130  
131      void (*noteOnHandler)(uint8_t) = nullptr;
132      void (*noteOffHandler)(uint8_t) = nullptr;
133  
134      static const int arp_len = 4;  // number of notes in an arp
135      static const uint8_t arp_count= 8;   // how many arps in "arps"
136      int8_t arps[arp_count][arp_len] = {
137        {0, 4, 7, 12},    // major
138        {0, 3, 7, 10},    // minor 7th
139        {0, 3, 6, 3},     // Diminished
140        {0, 5, 7, 12},    // Suspended 4th
141        {0, 12, 0, -12},  // octaves
142        {0, 12, 24, -12}, // octaves 2
143        {0, -12, -12, 0}, // octaves 3 (bass)
144        {0, 0, 0, 0},     // root
145      };
146  
147      // FIXME: how to programmatically set arps?
148  
149  };