# tone_player.py

# Raspberry Pi Pico - step sequencer demo using a speaker.

# This sample demonstrates mapping step sequencer events to tones generated on a
# speaker driven from a digital output via a transistor.  The default output is
# GP22, Raspberry Pi Pico pin 29.

#--------------------------------------------------------------------------------
# Import standard modules.
import time

# Load the CircuitPython hardware definition module for pin definitions.
import board

# Import course modules.  These files should be copied to the top-level
# directory of the CIRCUITPY filesystem on the Pico.
import tones
import sequencer
import remote

#---------------------------------------------------------------
class BeatSpeaker:
    def __init__(self, speaker):
        """Create musical beats on a speaker sequencer event callbacks.

        :param drv8833 speaker: speaker driver object to use for output
        """
        self.speaker = speaker
        self.timeout_timer = 0       # nanosecond timer for timing out motions

    def note_event(self, char):
        """Callback to receive sequencer events encoded as single characters."""

        # Whitespace or period will be treated as a rest.
        if (char.isspace() or char == '.'):
            self.speaker.noTone()
        else:
            # Use the character value to set the movement magnitude.  This could
            # be considerably elaborated to produce varied motions based on the
            # 'note' value.
            if char in 'abcdefg':
                # convert char code into a MIDI value
                midi_note = ord(char) - ord('a') + 69

                # note: our typical tiny speakers cannot handle full power
                self.speaker.midi_tone(midi_note, 0.01)
                self.timeout_timer = int(0.5 * 1e9)
            else:
                # convert char code into a MIDI value
                midi_note = ord(char) - ord('A') + 69

                # note: our typical tiny speakers cannot handle full power
                self.speaker.midi_tone(midi_note, 0.03)
                self.timeout_timer = int(0.5 * 1e9)

    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."""

        # Apply a duration limit to any movement.
        if self.timeout_timer > 0:
            self.timeout_timer -= elapsed
            if self.timeout_timer <= 0:
                self.timeout_timer = 0
                self.speaker.noTone()

#--------------------------------------------------------------------------------
# Generator function to yield individual characters from a file.  The generator
# returned by this function may be passed as an iterator to the sequencer
# set_sequence() method.

def file_char_iterator(path):
    with open(path, 'r') as input:
        for line in input:
            for char in line:
                if char != '\n':
                    yield char

#--------------------------------------------------------------------------------
# Create an object to represent the speaker driver.
speaker = tones.ToneSpeaker(board.GP22)

# Create beat motion control connected to the servo.
instrument = BeatSpeaker(speaker)

# Create a sequencer and connect it to the speaker instrument control.
sequencer = sequencer.Sequencer()
sequencer.set_note_handler(instrument.note_event)

# Set a test pattern to loop.
sequencer.set_pattern("C   G   C   E c A   c g E   Aeab")   

# Alternatively, stream a musical pattern from a file into the sequencer.
# sequencer.set_sequence(file_char_iterator('notes.txt'))

# Set up communication interface and callbacks.
remote  = remote.RemoteSerial()

def default_handler(msgtype, *args):
    print(f"Warning: received unknown message {msgtype} {args}")

remote.add_default_handler(default_handler)    
remote.add_handler('tempo', sequencer.set_tempo)
remote.add_handler('pattern', sequencer.set_pattern)

#---------------------------------------------------------------
# Main event loop to run each non-preemptive thread.

last_clock = time.monotonic_ns()

while True:
    # read the current nanosecond clock
    now = time.monotonic_ns()
    elapsed = now - last_clock
    last_clock = now

    # poll each thread
    remote.poll(elapsed)    
    sequencer.poll(elapsed)    
    instrument.poll(elapsed)
