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# metronome.py
 2
 3# Raspberry Pi Pico - demonstrate rhythmic servo motion like a metronome 
 4
 5# This assumes a tiny 9G servo has been wired up to the Pico as follows:
 6#   Pico pin 40 (VBUS)  -> servo red   (+5V)
 7#   Pico pin 38 (GND)   -> servo brown (GND)
 8#   Pico pin 1  (GP0)   -> servo orange (SIG)
 9#--------------------------------------------------------------------------------
10# Import standard modules.
11import time
12
13# Load the CircuitPython hardware definition module for pin definitions.
14import board
15
16# Import course modules.  These files should be copied to the top-level
17# directory of the CIRCUITPY filesystem on the Pico.
18import servo
19
20#--------------------------------------------------------------------------------
21class Metronome:
22    def __init__(self, servo):
23        """Implement a simple bang-bang metronome motion on a given servo.  Intended to
24        run within an event loop. The servo moves once per beat.
25        """
26        self.servo = servo
27        self.range = [45, 135]
28        self.state = False
29        self.update_timer = 0
30        self.set_tempo(60)
31            
32    def poll(self, elapsed):
33        """Polling function to be called as frequently as possible from the event loop
34        with the nanoseconds elapsed since the last cycle."""
35        self.update_timer -= elapsed
36        if self.update_timer < 0:
37            self.update_timer += self.update_interval
38            if self.state:
39                self.servo.write(self.range[0])
40                self.state = False
41            else:
42                self.servo.write(self.range[1])
43                self.state = True
44                
45    def set_tempo(self, bpm):
46        """Set the metronome tempo in beats per minute."""
47        # Calculate the cycle period in nanoseconds.
48        self.update_interval = int(60e9 / bpm)
49
50#--------------------------------------------------------------------------------
51# Create an object to represent a servo on the given hardware pin.
52servo = servo.Servo(board.GP0)
53
54# Create a metronome controller attached to the servo.
55metronome = Metronome(servo)
56
57#---------------------------------------------------------------
58# Main event loop to run each non-preemptive thread.
59
60last_clock = time.monotonic_ns()
61
62while True:
63    # read the current nanosecond clock
64    now = time.monotonic_ns()
65    elapsed = now - last_clock
66    last_clock = now
67
68    # poll each thread
69    metronome.poll(elapsed)    
70    

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# sequencer.py
 2
 3# CircuitPython - Step Sequencer
 4
 5# This module provides a class for generating timed events based on characters
 6# received from an iterator.  With a character string as pattern input, this
 7# implements a looping 'step sequencer' in which each character represents an
 8# event within a given time slot.  The iterator may also be a list, tuple,
 9# generator function, etc., in which case a loop callback may be issued when the
10# sequence ends.  The code is purely computational and so does not depend upon
11# any specific hardware features.
12
13#--------------------------------------------------------------------------------
14class Sequencer:
15    def __init__(self):
16        """Implement an event sequencer which can be used as a step sequencer.  Intended
17        to run within an event loop.
18        """
19        self.sequence = None        # current iterable representing a sequence
20        self.pattern = None         # may hold a string to loop
21        self.note_handler = None    # callback to receive each timed event
22        self.loop_handler = None    # callback to indicate the sequence has completed
23        self.subdivision  = 4       # number of updates per beat, defaults to sixteenth notes
24        self.set_tempo(60)          # initialize beat tempo in beats per minute
25        self.update_timer = 0       # nanosecond timer between subdivision ticks
26        
27    def set_tempo(self, bpm):
28        """Set the metronome tempo in beats per minute."""
29        # Calculate the subdivision cycle period in nanoseconds.
30        self.update_interval = int(60e9 / (self.subdivision * bpm))
31        self.tempo = bpm
32
33    def set_note_handler(self, handler):
34        """Set the callback for events, which will be called with a single character
35        string argument."""
36        self.note_handler = handler
37
38    def set_loop_handler(self, handler):
39        """Set the callback for the end of the loop.  The handler will not be called for
40        string patterns, as they automatically loop.
41        """
42        self.loop_handler = handler
43
44    def set_pattern(self, string):
45        """Set a pattern string, which will automatically loop."""
46        self.pattern = string
47        self.sequence = iter(string)
48
49    def set_sequence(self, iterable):
50        """Set a sequence using an iterable which returns characters.  This will not
51        automatically loop, the loop handler will be called if provided.
52        """
53        self.pattern = None
54        self.sequence = iterable
55
56    def poll(self, elapsed):
57        """Polling function to be called as frequently as possible from the event loop
58        with the nanoseconds elapsed since the last cycle."""
59        self.update_timer -= elapsed
60        if self.update_timer < 0:
61            self.update_timer += self.update_interval
62
63            # fetch the next character from the event iterable
64            if self.sequence is not None:
65                try:
66                    char = next(self.sequence)
67                    if self.note_handler is not None:
68                        self.note_handler(char)
69
70                except StopIteration:
71                    # if the current sequence is exhausted, check whether a patttern
72                    # string can be looped or another sequence started
73                    if self.pattern is not None:
74                        self.sequence = iter(self.pattern)
75
76                    elif self.loop_handler is not None:
77                        self.sequence = self.loop_handler()
78
79                    else:
80                        self.sequence = None
81
82                    # if a new sequence is available:
83                    if self.sequence is not None:
84                        try:
85                            char = next(self.sequence)
86                            if self.note_handler is not None:
87                                self.note_handler(char)
88                        except StopIteration:
89                            # if the new sequence fails to produce an item, just stop
90                            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# tones.py
 2#
 3# CircuitPython - speaker PWM driver
 4#
 5# This module provides a class for generating audible tones using a PWM digital
 6# output, analogous to the Arduino tone() and noTone() capability.
 7# CircuitPython is capable for more sophisticated sample-rate PWM modulation to
 8# generate waveforms, but this is adequate for simple tone signaling and
 9# melodies.
10#
11# links to CircuitPython module documentation:
12# pwmio   https://circuitpython.readthedocs.io/en/latest/shared-bindings/pwmio/index.html
13
14################################################################################
15# Load the standard math module.
16import math
17
18# Load the CircuitPython pulse-width-modulation module for driving hardware.
19import pwmio
20
21#--------------------------------------------------------------------------------
22
23class ToneSpeaker():
24    def __init__(self, pin):
25        """Interface for generating simple tones using a single speaker circuit on a
26        digital output.  This holds the underlying PWM output object and
27        provides convenience methods for setting state.  N.B. this does not
28        implement any other timing process, in particular it starts and stops
29        tones but does not control duration.
30
31        The creation of the object also initializes the physical pin hardware.
32        Normally the speaker will be driven by an amplification stage, e.g. a MOSFET
33        transistor or ULN2003 bipolar driver.
34        """
35
36        # Create a PWMOut object on the desired pin to drive the speaker.
37        # Note that the initial duty cycle of zero generates no pulses, which
38        # for many servos will present as a quiescent low-power state.
39        self.pwm = pwmio.PWMOut(pin, duty_cycle=0, frequency=440, variable_frequency=True)
40
41    def deinit(self):
42        """Object lifecycle protocol method."""
43        self.pwm.deinit()
44        self.pwm = None
45        
46    def tone(self, frequency, amplitude=1.0):
47        """Enable generation of an audible tone on the attached digital output.
48
49        :param frequency: pitch in Hz (cycles/sec)
50        :param amplitude: fraction of full duty cycle (0.0 to 1.0)
51        """
52
53        # Set the frequency.
54        self.pwm.frequency = int(frequency)
55        
56        # Calculate the desired duty cycle as a 16-bit fixed point integer.
57        # Full amplitude yields a 50% duty square wave.
58        self.pwm.duty_cycle = int(amplitude * 2**15)
59
60    def noTone(self):
61        """Stop any playing tone."""
62        self.pwm.duty_cycle = 0
63        
64    def midi_to_freq(self, midi_note):
65        """Convert an integer MIDI note value to frequency using an equal temperament
66        tuning."""
67        #           A0 has MIDI note value 21 and frequency  27.50 Hz
68        # Middle-C  C4 has MIDI note value 60 and frequency 261.63 Hz
69        # Concert   A4 has MIDI note value 69 and frequency 440.00 Hz
70        return 27.5 * math.pow(2.0, (midi_note - 21) / 12)
71
72    def midi_tone(self, midi_note, amplitude=1.0):
73        """Start a tone using the integer MIDI key value to specify frequency."""
74        self.tone(self.midi_to_freq(midi_note), amplitude)
75        

Tone Player Demo

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

Direct download: tone_player.py.

  1# tone_player.py
  2
  3# Raspberry Pi Pico - step sequencer demo using a speaker.
  4
  5# This sample demonstrates mapping step sequencer events to tones generated on a
  6# speaker driven from a digital output via a transistor.  The default output is
  7# GP22, Raspberry Pi Pico pin 29.
  8
  9#--------------------------------------------------------------------------------
 10# Import standard modules.
 11import time
 12
 13# Load the CircuitPython hardware definition module for pin definitions.
 14import board
 15
 16# Import course modules.  These files should be copied to the top-level
 17# directory of the CIRCUITPY filesystem on the Pico.
 18import tones
 19import sequencer
 20import remote
 21
 22#---------------------------------------------------------------
 23class BeatSpeaker:
 24    def __init__(self, speaker):
 25        """Create musical beats on a speaker sequencer event callbacks.
 26
 27        :param drv8833 speaker: speaker driver object to use for output
 28        """
 29        self.speaker = speaker
 30        self.timeout_timer = 0       # nanosecond timer for timing out motions
 31
 32    def note_event(self, char):
 33        """Callback to receive sequencer events encoded as single characters."""
 34
 35        # Whitespace or period will be treated as a rest.
 36        if (char.isspace() or char == '.'):
 37            self.speaker.noTone()
 38        else:
 39            # Use the character value to set the movement magnitude.  This could
 40            # be considerably elaborated to produce varied motions based on the
 41            # 'note' value.
 42            if char in 'abcdefg':
 43                # convert char code into a MIDI value
 44                midi_note = ord(char) - ord('a') + 69
 45
 46                # note: our typical tiny speakers cannot handle full power
 47                self.speaker.midi_tone(midi_note, 0.01)
 48                self.timeout_timer = int(0.5 * 1e9)
 49            else:
 50                # convert char code into a MIDI value
 51                midi_note = ord(char) - ord('A') + 69
 52
 53                # note: our typical tiny speakers cannot handle full power
 54                self.speaker.midi_tone(midi_note, 0.03)
 55                self.timeout_timer = int(0.5 * 1e9)
 56
 57    def poll(self, elapsed):
 58        """Polling function to be called as frequently as possible from the event loop
 59        with the nanoseconds elapsed since the last cycle."""
 60
 61        # Apply a duration limit to any movement.
 62        if self.timeout_timer > 0:
 63            self.timeout_timer -= elapsed
 64            if self.timeout_timer <= 0:
 65                self.timeout_timer = 0
 66                self.speaker.noTone()
 67
 68#--------------------------------------------------------------------------------
 69# Generator function to yield individual characters from a file.  The generator
 70# returned by this function may be passed as an iterator to the sequencer
 71# set_sequence() method.
 72
 73def file_char_iterator(path):
 74    with open(path, 'r') as input:
 75        for line in input:
 76            for char in line:
 77                if char != '\n':
 78                    yield char
 79
 80#--------------------------------------------------------------------------------
 81# Create an object to represent the speaker driver.
 82speaker = tones.ToneSpeaker(board.GP22)
 83
 84# Create beat motion control connected to the servo.
 85instrument = BeatSpeaker(speaker)
 86
 87# Create a sequencer and connect it to the speaker instrument control.
 88sequencer = sequencer.Sequencer()
 89sequencer.set_note_handler(instrument.note_event)
 90
 91# Set a test pattern to loop.
 92sequencer.set_pattern("C   G   C   E c A   c g E   Aeab")   
 93
 94# Alternatively, stream a musical pattern from a file into the sequencer.
 95# sequencer.set_sequence(file_char_iterator('notes.txt'))
 96
 97# Set up communication interface and callbacks.
 98remote  = remote.RemoteSerial()
 99
100def default_handler(msgtype, *args):
101    print(f"Warning: received unknown message {msgtype} {args}")
102
103remote.add_default_handler(default_handler)    
104remote.add_handler('tempo', sequencer.set_tempo)
105remote.add_handler('pattern', sequencer.set_pattern)
106
107#---------------------------------------------------------------
108# Main event loop to run each non-preemptive thread.
109
110last_clock = time.monotonic_ns()
111
112while True:
113    # read the current nanosecond clock
114    now = time.monotonic_ns()
115    elapsed = now - last_clock
116    last_clock = now
117
118    # poll each thread
119    remote.poll(elapsed)    
120    sequencer.poll(elapsed)    
121    instrument.poll(elapsed)

Beat Player Demo

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

Direct download: beat_player.py.

 1# beat_player.py
 2
 3# Raspberry Pi Pico - Rhythmic Step Sequencer demo
 4
 5# This assumes a tiny 9G servo has been wired up to the Pico as follows:
 6#   Pico pin 40 (VBUS)  -> servo red   (+5V)
 7#   Pico pin 38 (GND)   -> servo brown (GND)
 8#   Pico pin 1  (GP0)   -> servo orange (SIG)
 9
10#--------------------------------------------------------------------------------
11# Import standard modules.
12import time
13
14# Load the CircuitPython hardware definition module for pin definitions.
15import board
16
17# Import course modules.  These files should be copied to the top-level
18# directory of the CIRCUITPY filesystem on the Pico.
19import servo
20import sequencer
21import remote
22
23#---------------------------------------------------------------
24# Define motion control class to execute sequencer event callbacks on a hobby servo.
25class BeatServo:
26    def __init__(self, servo):
27        """Create musical beats on a hobby servo."""
28        self.servo = servo
29        self.intensity = 45
30        self.state = False
31
32    def note_event(self, char):
33        """Callback to receive sequencer events encoded as single characters."""
34
35        # ignore whitespace or period, these will be treated as a rest
36        if not (char.isspace() or char == '.'):
37            # use the character value to set the movement magnitude
38            if char in '+abcdefg':
39                self.intensity = 15
40            else:
41                self.intensity = 60
42
43            # toggle the servo state
44            if self.state:
45                self.servo.write(90 + self.intensity)
46                self.state = False
47            else:
48                self.servo.write(90 - self.intensity)
49                self.state = True
50            
51    def poll(self, elapsed):
52        """This object doesn't yet need a polling function, for now it only updates on
53        callback events.
54        """
55        pass
56            
57#--------------------------------------------------------------------------------
58# Create an object to represent a servo on the given hardware pin.
59servo = servo.Servo(board.GP0)
60
61# Create beat motion control connected to the servo.
62motion = BeatServo(servo)
63
64# Create a sequencer and connect it to the servo motion control.
65sequencer = sequencer.Sequencer()
66sequencer.set_note_handler(motion.note_event)
67
68# Set a test pattern to loop.
69sequencer.set_pattern("#   #   #   +   +   + + +   #+++")   
70
71# Set up communication interface and callbacks.
72remote  = remote.RemoteSerial()
73
74def default_handler(msgtype, *args):
75    print(f"Warning: received unknown message {msgtype} {args}")
76
77remote.add_default_handler(default_handler)    
78remote.add_handler('tempo', sequencer.set_tempo)
79remote.add_handler('pattern', sequencer.set_pattern)
80
81#---------------------------------------------------------------
82# Main event loop to run each non-preemptive thread.
83
84last_clock = time.monotonic_ns()
85
86while True:
87    # read the current nanosecond clock
88    now = time.monotonic_ns()
89    elapsed = now - last_clock
90    last_clock = now
91
92    # poll each thread
93    remote.poll(elapsed)    
94    sequencer.poll(elapsed)    
95    

Spin Player Demo

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

Direct download: spin_player.py.

  1# spin_player.py
  2
  3# Raspberry Pi Pico - step sequencer demo using DC motors.
  4
  5# This sample demonstrates mapping step sequencer events to DC motor
  6# activations.  This assumes a DRV8833 driver has been attached to the default
  7# pins.  Please see the drv8833 module documentation for details.
  8
  9#--------------------------------------------------------------------------------
 10# Import standard modules.
 11import time
 12
 13# Load the CircuitPython hardware definition module for pin definitions.
 14import board
 15
 16# Import course modules.  These files should be copied to the top-level
 17# directory of the CIRCUITPY filesystem on the Pico.
 18import drv8833
 19import sequencer
 20import remote
 21
 22#---------------------------------------------------------------
 23class BeatMotor:
 24    def __init__(self, motor):
 25        """Create musical beats on two DC motors using sequencer event callbacks.  This
 26         will need to be customized to produce appropriate movements for the
 27         attached mechanical hardware.
 28
 29        :param drv8833 motor: motor driver object to use for output
 30        """
 31        self.motor = motor
 32        self.timeout_timer = 0       # nanosecond timer for timing out motions
 33
 34    def note_event(self, char):
 35        """Callback to receive sequencer events encoded as single characters."""
 36
 37        # Whitespace or period will be treated as a rest.
 38        if (char.isspace() or char == '.'):
 39            self.motor.write(0, 0.0)
 40            self.motor.write(1, 0.0)
 41        else:
 42            # Use the character value to set the movement magnitude.  This could
 43            # be considerably elaborated to produce varied motions based on the
 44            # 'note' value.
 45            if char in '+abcdefg':
 46                self.motor.write(0, 0.75)
 47                self.motor.write(1, -0.75)
 48                self.timeout_timer = int(1e9)
 49            else:
 50                self.motor.write(0, -1.0)
 51                self.motor.write(1, 1.0)
 52                self.timeout_timer = int(1e9)                
 53
 54    def poll(self, elapsed):
 55        """Polling function to be called as frequently as possible from the event loop
 56        with the nanoseconds elapsed since the last cycle."""
 57
 58        # Apply a duration limit to any movement.
 59        if self.timeout_timer > 0:
 60            self.timeout_timer -= elapsed
 61            if self.timeout_timer <= 0:
 62                self.timeout_timer = 0
 63                self.motor.write(0, 0.0)
 64                self.motor.write(1, 0.0)
 65
 66#--------------------------------------------------------------------------------
 67# Create an object to represent the motor driver on the default hardware pins.
 68motor = drv8833.DRV8833()
 69
 70# Create beat motion control connected to the servo.
 71motion = BeatMotor(motor)
 72
 73# Create a sequencer and connect it to the motor motion control.
 74sequencer = sequencer.Sequencer()
 75sequencer.set_note_handler(motion.note_event)
 76
 77# Set a test pattern to loop.
 78sequencer.set_pattern("#   #   #   +   +   + + +   #+++")   
 79
 80# Set up communication interface and callbacks.
 81remote  = remote.RemoteSerial()
 82
 83def default_handler(msgtype, *args):
 84    print(f"Warning: received unknown message {msgtype} {args}")
 85
 86remote.add_default_handler(default_handler)    
 87remote.add_handler('tempo', sequencer.set_tempo)
 88remote.add_handler('pattern', sequencer.set_pattern)
 89
 90#---------------------------------------------------------------
 91# Main event loop to run each non-preemptive thread.
 92
 93last_clock = time.monotonic_ns()
 94
 95while True:
 96    # read the current nanosecond clock
 97    now = time.monotonic_ns()
 98    elapsed = now - last_clock
 99    last_clock = now
100
101    # poll each thread
102    remote.poll(elapsed)    
103    sequencer.poll(elapsed)    
104    motion.poll(elapsed)