# sequencer.py

# CircuitPython - Step Sequencer

# This module provides a class for generating timed events based on characters
# received from an iterator.  With a character string as pattern input, this
# implements a looping 'step sequencer' in which each character represents an
# event within a given time slot.  The iterator may also be a list, tuple,
# generator function, etc., in which case a loop callback may be issued when the
# sequence ends.  The code is purely computational and so does not depend upon
# any specific hardware features.

#--------------------------------------------------------------------------------
class Sequencer:
    def __init__(self):
        """Implement an event sequencer which can be used as a step sequencer.  Intended
        to run within an event loop.
        """
        self.sequence = None        # current iterable representing a sequence
        self.pattern = None         # may hold a string to loop
        self.note_handler = None    # callback to receive each timed event
        self.loop_handler = None    # callback to indicate the sequence has completed
        self.subdivision  = 4       # number of updates per beat, defaults to sixteenth notes
        self.set_tempo(60)          # initialize beat tempo in beats per minute
        self.update_timer = 0       # nanosecond timer between subdivision ticks
        
    def set_tempo(self, bpm):
        """Set the metronome tempo in beats per minute."""
        # Calculate the subdivision cycle period in nanoseconds.
        self.update_interval = int(60e9 / (self.subdivision * bpm))
        self.tempo = bpm

    def set_note_handler(self, handler):
        """Set the callback for events, which will be called with a single character
        string argument."""
        self.note_handler = handler

    def set_loop_handler(self, handler):
        """Set the callback for the end of the loop.  The handler will not be called for
        string patterns, as they automatically loop.
        """
        self.loop_handler = handler

    def set_pattern(self, string):
        """Set a pattern string, which will automatically loop."""
        self.pattern = string
        self.sequence = iter(string)

    def set_sequence(self, iterable):
        """Set a sequence using an iterable which returns characters.  This will not
        automatically loop, the loop handler will be called if provided.
        """
        self.pattern = None
        self.sequence = iterable

    def poll(self, elapsed):
        """Polling function to be called as frequently as possible from the event loop
        with the nanoseconds elapsed since the last cycle."""
        self.update_timer -= elapsed
        if self.update_timer < 0:
            self.update_timer += self.update_interval

            # fetch the next character from the event iterable
            if self.sequence is not None:
                try:
                    char = next(self.sequence)
                    if self.note_handler is not None:
                        self.note_handler(char)

                except StopIteration:
                    # if the current sequence is exhausted, check whether a patttern
                    # string can be looped or another sequence started
                    if self.pattern is not None:
                        self.sequence = iter(self.pattern)

                    elif self.loop_handler is not None:
                        self.sequence = self.loop_handler()

                    else:
                        self.sequence = None

                    # if a new sequence is available:
                    if self.sequence is not None:
                        try:
                            char = next(self.sequence)
                            if self.note_handler is not None:
                                self.note_handler(char)
                        except StopIteration:
                            # if the new sequence fails to produce an item, just stop
                            self.sequence = None
