Music and Rhythm Examples - CircuitPython

The following short CircuitPython programs demonstrate rhythm and music generation. These are largely platform-neutral.

Related Pages

Metronome Example

This example demonstrates the essential elements of creating a pattern of movement over time within an event-loop programming structure.

Direct download: metronome.py.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# metronome.py

# Raspberry Pi Pico - demonstrate rhythmic servo motion like a metronome 

# This assumes a tiny 9G servo has been wired up to the Pico as follows:
#   Pico pin 40 (VBUS)  -> servo red   (+5V)
#   Pico pin 38 (GND)   -> servo brown (GND)
#   Pico pin 1  (GP0)   -> servo orange (SIG)
#--------------------------------------------------------------------------------
# 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 servo

#--------------------------------------------------------------------------------
class Metronome:
    def __init__(self, servo):
        """Implement a simple bang-bang metronome motion on a given servo.  Intended to
        run within an event loop. The servo moves once per beat.
        """
        self.servo = servo
        self.range = [45, 135]
        self.state = False
        self.update_timer = 0
        self.set_tempo(60)
            
    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
            if self.state:
                self.servo.write(self.range[0])
                self.state = False
            else:
                self.servo.write(self.range[1])
                self.state = True
                
    def set_tempo(self, bpm):
        """Set the metronome tempo in beats per minute."""
        # Calculate the cycle period in nanoseconds.
        self.update_interval = int(60e9 / bpm)

#--------------------------------------------------------------------------------
# Create an object to represent a servo on the given hardware pin.
servo = servo.Servo(board.GP0)

# Create a metronome controller attached to the servo.
metronome = Metronome(servo)

#---------------------------------------------------------------
# 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
    metronome.poll(elapsed)    
    

sequencer module

This module implements a general-purpose ‘step sequencer’ which delivers a regular series of events via callback functions. It is used as a component in several subsequent demos.

class sequencer.Sequencer

Implement an event sequencer which can be used as a step sequencer. Intended to run within an event loop.

Direct download: sequencer.py.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# 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

tones module

This module implements a basic speaker interface which can generate tones using the PWM library.

class tones.ToneSpeaker

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.

Direct download: tones.py.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# 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)
        

Tone Player Demo

This example demonstrates using the step sequencer to control a speaker.

Direct download: tone_player.py.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# 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)

Beat Player Demo

This example demonstrates using the step sequencer to control a hobby servo.

Direct download: beat_player.py.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# beat_player.py

# Raspberry Pi Pico - Rhythmic Step Sequencer demo

# This assumes a tiny 9G servo has been wired up to the Pico as follows:
#   Pico pin 40 (VBUS)  -> servo red   (+5V)
#   Pico pin 38 (GND)   -> servo brown (GND)
#   Pico pin 1  (GP0)   -> servo orange (SIG)

#--------------------------------------------------------------------------------
# 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 servo
import sequencer
import remote

#---------------------------------------------------------------
# Define motion control class to execute sequencer event callbacks on a hobby servo.
class BeatServo:
    def __init__(self, servo):
        """Create musical beats on a hobby servo."""
        self.servo = servo
        self.intensity = 45
        self.state = False

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

        # ignore whitespace or period, these will be treated as a rest
        if not (char.isspace() or char == '.'):
            # use the character value to set the movement magnitude
            if char in '+abcdefg':
                self.intensity = 15
            else:
                self.intensity = 60

            # toggle the servo state
            if self.state:
                self.servo.write(90 + self.intensity)
                self.state = False
            else:
                self.servo.write(90 - self.intensity)
                self.state = True
            
    def poll(self, elapsed):
        """This object doesn't yet need a polling function, for now it only updates on
        callback events.
        """
        pass
            
#--------------------------------------------------------------------------------
# Create an object to represent a servo on the given hardware pin.
servo = servo.Servo(board.GP0)

# Create beat motion control connected to the servo.
motion = BeatServo(servo)

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

# Set a test pattern to loop.
sequencer.set_pattern("#   #   #   +   +   + + +   #+++")   

# 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)    
    

Spin Player Demo

This example demonstrates using the step sequencer to control a pair of DC motors.

Direct download: spin_player.py.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
# spin_player.py

# Raspberry Pi Pico - step sequencer demo using DC motors.

# This sample demonstrates mapping step sequencer events to DC motor
# activations.  This assumes a DRV8833 driver has been attached to the default
# pins.  Please see the drv8833 module documentation for details.

#--------------------------------------------------------------------------------
# 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 drv8833
import sequencer
import remote

#---------------------------------------------------------------
class BeatMotor:
    def __init__(self, motor):
        """Create musical beats on two DC motors using sequencer event callbacks.  This
         will need to be customized to produce appropriate movements for the
         attached mechanical hardware.

        :param drv8833 motor: motor driver object to use for output
        """
        self.motor = motor
        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.motor.write(0, 0.0)
            self.motor.write(1, 0.0)
        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':
                self.motor.write(0, 0.75)
                self.motor.write(1, -0.75)
                self.timeout_timer = int(1e9)
            else:
                self.motor.write(0, -1.0)
                self.motor.write(1, 1.0)
                self.timeout_timer = int(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.motor.write(0, 0.0)
                self.motor.write(1, 0.0)

#--------------------------------------------------------------------------------
# Create an object to represent the motor driver on the default hardware pins.
motor = drv8833.DRV8833()

# Create beat motion control connected to the servo.
motion = BeatMotor(motor)

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

# Set a test pattern to loop.
sequencer.set_pattern("#   #   #   +   +   + + +   #+++")   

# 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)    
    motion.poll(elapsed)