Piano

Piano

A MicroPython code example running on a Raspberry Pi Pico 2 W microcontroller connected to a passive piezo buzzer, an OLED display module (SSD1306) and 6 push buttons on a breadboard to play different musical notes on the buzzer, draw the notes and musical staff lines on the display, and replay as a recording after a pause

We use the buttons to play different musical notes. We also learn how to draw shapes on the OLED screen to draw the notes on musical staff lines. Finally, we save the notes in a list along with their duration played. And when user input pauses, we replay the notes to create a fun musical instrument.

We read 6 different inputs for the 6 buttons. For this, we use a list as shown below:

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
from machine import Pin
from time import sleep, ticks_ms, ticks_diff
from buzzer import Buzzer

buzzer = Buzzer(pwm_pin=16)

note = "Silence"
notes = []
buttons = []
start_time = ticks_ms()
buzzer.melody_note_gap = 0

# Add buttons for each buzzer note C4, D4, E4, F4, G4, A4
# connected to pins 0, 1, 2, 3, 4, 5 respectively
for i, note in enumerate(buzzer.notes): buttons.append(Pin(i, Pin.IN, Pin.PULL_UP))

def current_note_duration():
    return ticks_diff(ticks_ms(), start_time) / 1000

def current_note():
    note = "Silence"
    for i, key in enumerate(buzzer.notes):
        if buttons[i].value() == 0:
            note = key        
    return note

while True:
    note = current_note()
    note_duration = current_note_duration()
    buzzer.play_note(note)

To further learn drawing shapes on the OLED screen, we draw the shape of the notes as they are played, by creating a function draw_notes_on_screen()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def draw_notes_on_screen():
    global note, notes
    notes.append(note) # save note in list
    if len(notes) > 7: notes.pop(0) # remove oldest note if list grows past 7 notes
    oled.display.fill(0) # clear display
    for i in range(5): oled.display.hline(0, 8*i + 8, 128, 1) # draw 5 staff lines
    for position, note in enumerate(notes): 
        draw_note(position, note) # draw each note at its position
    oled.display.show()

def draw_note(position, note):
    x = 16 + 16*position
    y = 48 - 4 * buzzer.notes.index(note)
    oled.display.ellipse(x - 6, y, 6, 4, 1, True) # note ellipse
    oled.display.vline(x, y - 28, 28, 1) # note vertical line
    if note == "C4": oled.display.hline(x - 16, y, 20, 1) # add a small horizontal line for C4

Finally, we save the notes in a list along with their duration played. And when user input pauses, we replay the notes to create a fun musical instrument. For this, we add some more helper functions below.

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
recording = []

def remove_empty_notes_from_start_and_end():
    global recording
    if recording[0].startswith("Silence"): recording.pop(0)
    if recording[-1].startswith("Silence"): recording.pop()

def add_last_note_to_recording():
    global recording, start_time
    last_note = value_filter.previous_value
    last_note_duration = ticks_diff(ticks_ms(), start_time) / 1000
    recording.append(f"{last_note} {last_note_duration}")
    start_time = ticks_ms()
    
def reset_recording():
    global recording, notes, start_time
    recording.clear()
    notes.clear()
    start_time = ticks_ms()

def replay_recording():
    remove_empty_notes_from_start_and_end()
    oled.print("Replaying...")
    buzzer.play_melody(*recording) # replay recording
    reset_recording() # reset to start recording again
    oled.print("Ready")

With all these functions, our main loop is still easy to follow.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    while True:
        note = current_note()
        note_duration = current_note_duration()
        buzzer.play_note(note)
        
        # If stopped playing for 2 seconds, and anything was recorded, replay the recording
        if note == "Silence" and note_duration > 2 and recording: replay_recording()
        
        if value_filter.did_change(note): # when note changed or ended
            
            # Save note in recording list to replay later
            add_last_note_to_recording()
            
            # When a new note is pressed, append to notes and draw notes
            if note != "Silence": draw_notes_on_screen()

To put it all together, and test, create a new file in Thonny, with file name piano.py. Copy and paste the code below into the file you created.

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
print("\nPiano")

# DESCRIPTION

# We use the buttons to play different musical notes. We also learn how to 
# draw shapes on the OLED screen to draw the notes on musical staff lines.
# Finally, we save the notes in a list along with their duration played. And
# when user input pauses, we replay the notes to create a fun musical instrument.

# CONNECTIONS REQUIRED

# No additional connections are needed

from time import sleep, ticks_ms, ticks_diff
from buzzer import Buzzer
from oled import OLED
from machine import Pin
from value_filter import ValueFilter

buzzer = Buzzer(pwm_pin=16)
oled = OLED(scl_pin=15, sda_pin=14)
value_filter = ValueFilter(initial_value="Silence")

note = "Silence"
notes = []
buttons = []
recording = []
start_time = ticks_ms()
oled.print("Ready")
buzzer.melody_note_gap = 0

# Add buttons for each buzzer note C4, D4, E4, F4, G4, A4
# connected to pins 0, 1, 2, 3, 4, 5 respectively
for i, note in enumerate(buzzer.notes): buttons.append(Pin(i, Pin.IN, Pin.PULL_UP))

def remove_empty_notes_from_start_and_end():
    global recording
    if recording[0].startswith("Silence"): recording.pop(0)
    if recording[-1].startswith("Silence"): recording.pop()

def add_last_note_to_recording():
    global recording, start_time
    last_note = value_filter.previous_value
    last_note_duration = ticks_diff(ticks_ms(), start_time) / 1000
    recording.append(f"{last_note} {last_note_duration}")
    start_time = ticks_ms()
    
def reset_recording():
    global recording, notes, start_time
    recording.clear()
    notes.clear()
    start_time = ticks_ms()

def replay_recording():
    remove_empty_notes_from_start_and_end()
    oled.print("Replaying...")
    buzzer.play_melody(*recording) # replay recording
    reset_recording() # reset to start recording again
    oled.print("Ready")

def current_note_duration():
    return ticks_diff(ticks_ms(), start_time) / 1000

def current_note():
    note = "Silence"
    for i, key in enumerate(buzzer.notes):
        if buttons[i].value() == 0:
            note = key        
    return note

def draw_notes_on_screen():
    global note, notes
    notes.append(note) # save note in list
    if len(notes) > 7: notes.pop(0) # remove oldest note if list grows past 7 notes
    oled.display.fill(0) # clear display
    for i in range(5): oled.display.hline(0, 8*i + 8, 128, 1) # draw 5 staff lines
    for position, note in enumerate(notes): 
        draw_note(position, note) # draw each note at its position
    oled.display.show()

def draw_note(position, note):
    x = 16 + 16*position
    y = 48 - 4 * buzzer.notes.index(note)
    oled.display.ellipse(x - 6, y, 6, 4, 1, True) # note ellipse
    oled.display.vline(x, y - 28, 28, 1) # note vertical line
    if note == "C4": oled.display.hline(x - 16, y, 20, 1) # add a small horizontal line for C4

try:
    while True:
        note = current_note()
        note_duration = current_note_duration()
        buzzer.play_note(note)
        
        # If stopped playing for 2 seconds, and anything was recorded, replay the recording
        if note == "Silence" and note_duration > 2 and recording: replay_recording()
        
        if value_filter.did_change(note): # when note changed or ended
            
            # Save note in recording list to replay later
            add_last_note_to_recording()
            
            # When a new note is pressed, append to notes and draw notes
            if note != "Silence": draw_notes_on_screen()
            
except KeyboardInterrupt:
    print("\nInterrupted by user!")

finally:
    oled.reset()
    buzzer.reset()