# tones.py
#
# CircuitPython - speaker PWM driver
#
# This module provides a class for generating audible tones using a PWM digital
# output, analogous to the Arduino tone() and noTone() capability.
# CircuitPython is capable for more sophisticated sample-rate PWM modulation to
# generate waveforms, but this is adequate for simple tone signaling and
# melodies.
#
# links to CircuitPython module documentation:
# pwmio   https://circuitpython.readthedocs.io/en/latest/shared-bindings/pwmio/index.html

################################################################################
# Load the standard math module.
import math

# Load the CircuitPython pulse-width-modulation module for driving hardware.
import pwmio

#--------------------------------------------------------------------------------

class ToneSpeaker():
    def __init__(self, pin):
        """Interface for generating simple tones using a single speaker circuit on a
        digital output.  This holds the underlying PWM output object and
        provides convenience methods for setting state.  N.B. this does not
        implement any other timing process, in particular it starts and stops
        tones but does not control duration.

        The creation of the object also initializes the physical pin hardware.
        Normally the speaker will be driven by an amplification stage, e.g. a MOSFET
        transistor or ULN2003 bipolar driver.
        """

        # Create a PWMOut object on the desired pin to drive the speaker.
        # Note that the initial duty cycle of zero generates no pulses, which
        # for many servos will present as a quiescent low-power state.
        self.pwm = pwmio.PWMOut(pin, duty_cycle=0, frequency=440, variable_frequency=True)

    def deinit(self):
        """Object lifecycle protocol method."""
        self.pwm.deinit()
        self.pwm = None
        
    def tone(self, frequency, amplitude=1.0):
        """Enable generation of an audible tone on the attached digital output.

        :param frequency: pitch in Hz (cycles/sec)
        :param amplitude: fraction of full duty cycle (0.0 to 1.0)
        """

        # Set the frequency.
        self.pwm.frequency = int(frequency)
        
        # Calculate the desired duty cycle as a 16-bit fixed point integer.
        # Full amplitude yields a 50% duty square wave.
        self.pwm.duty_cycle = int(amplitude * 2**15)

    def noTone(self):
        """Stop any playing tone."""
        self.pwm.duty_cycle = 0
        
    def midi_to_freq(self, midi_note):
        """Convert an integer MIDI note value to frequency using an equal temperament
        tuning."""
        #           A0 has MIDI note value 21 and frequency  27.50 Hz
        # Middle-C  C4 has MIDI note value 60 and frequency 261.63 Hz
        # Concert   A4 has MIDI note value 69 and frequency 440.00 Hz
        return 27.5 * math.pow(2.0, (midi_note - 21) / 12)

    def midi_tone(self, midi_note, amplitude=1.0):
        """Start a tone using the integer MIDI key value to specify frequency."""
        self.tone(self.midi_to_freq(midi_note), amplitude)
        
