Music and Rhythm Examples - CircuitPython¶
The following short CircuitPython programs demonstrate rhythm and music generation. These are largely platform-neutral.
Related Pages
Contents
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)
|