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)