Skip to content

Commit f0a73a4

Browse files
committed
Add working 4 voice square wave synth plus basic jukebox
1 parent c94ee11 commit f0a73a4

54 files changed

Lines changed: 9076 additions & 252 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

05_advanced/code/MusicPlayer.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
from machine import Pin, PWM, Timer
2+
3+
"""
4+
Plays music using PWM pins.
5+
On ESP32-S3 it seems that you can only half of the PWM channels at a time, leading to a maximum of 4 voices.
6+
Unsure if this limit is a bug in the logic a limitation of the python library or some other limitation.
7+
8+
Use `load_voice_groups_from_file` to load a song and to see the sheet music format.
9+
"""
10+
class MusicPlayer:
11+
def __init__(self, voice_groups, note_time, pins):
12+
self.voice_groups = voice_groups
13+
self.current_group = []
14+
self.note_time = note_time
15+
self.pins = pins
16+
self.voice_limit = len(pins)
17+
self.group_index = 0
18+
self.note_index = 0
19+
self.group_timer = Timer(0)
20+
self.pwms = [[0, None]] * len(pins)
21+
self.playing = False
22+
print("PWM Count:", len(self.pwms))
23+
24+
def start(self):
25+
self.playing = True
26+
self.current_group = self.voice_groups.pop(0)
27+
self.play_next_note()
28+
29+
def play_next_note(self, t=None):
30+
if t:
31+
t.deinit()
32+
33+
if self.note_index >= len(self.current_group[0]):
34+
if not self.start_next_group():
35+
self.clean_up_pwms()
36+
print("Song done")
37+
return
38+
39+
if self.current_group:
40+
for i, voice in enumerate(self.current_group):
41+
if (i >= self.voice_limit):
42+
print("Voice limit reached, skipping voice", i + 1, "of", len(self.current_group))
43+
continue
44+
note = voice[self.note_index]
45+
pwm = self.pwms[i]
46+
sustain = True
47+
if pwm[0] != note and note != -1:
48+
if pwm[1] is not None:
49+
#print("PWM Deinit old note", pwm[0], "on voice", i + 1)
50+
pwm[1].deinit()
51+
sustain = False
52+
if sustain:
53+
pass
54+
elif note > 0:
55+
#print("PWM init, Playing note", note, "on voice", i + 1)
56+
pwm = PWM(self.pins[i], freq=note, duty=512)
57+
self.pwms[i] = [note, pwm]
58+
else:
59+
self.pwms[i] = [0, None]
60+
self.note_index += 1
61+
self.group_timer.deinit()
62+
self.group_timer.init(mode=Timer.ONE_SHOT, period=self.note_time, callback=lambda t: self.play_next_note(t))
63+
64+
def start_next_group(self):
65+
self.note_index = 0
66+
if self.voice_groups:
67+
self.current_group = self.voice_groups.pop(0)
68+
print(len(self.voice_groups), "voice groups left to play")
69+
return True
70+
return False
71+
72+
def clean_up_pwms(self):
73+
for pwm in self.pwms:
74+
if pwm[1] is not None:
75+
pwm[1].deinit()
76+
self.playing = False
77+
78+
def load_voice_groups_from_file(file_path):
79+
"""
80+
Reads a text file containing musical notes and returns groups of voices that should be played together.
81+
The number at the start of the line is the octave.
82+
Lower case letters are normal notes, upper case letters are sharp notes
83+
- is a rest note, which means no sound will be played for that note.
84+
> is a sustain note, which means the previous note will continue to play.
85+
5|e-e---e---c-e---g---------|
86+
4|a-a---a---a-a---b---------|
87+
4|F-F---F---F-F---g-------g-|
88+
3|------------------------g-|
89+
2|d-d---d---d-d---g-------g-|
90+
1|------------------------g-|
91+
Groups are seperated by empty lines.
92+
A voice is a list of frequencies to play, a voice must exist for each concurrent frequency to play.
93+
When the example above is parsed it will produce four voices.
94+
95+
This format was adapted (slightly) from the format provided by pianoletternotes.blogspot.com.
96+
"""
97+
note_line_groups = read_notes_from_file(file_path)
98+
return parse_note_line_groups(note_line_groups)
99+
100+
101+
def read_notes_from_file(file_path):
102+
note_lines_groups = []
103+
with open(file_path, 'r') as file:
104+
current_group = []
105+
note_lines_groups.append(current_group)
106+
for line in file:
107+
stripped_line = line.strip()
108+
if not stripped_line:
109+
current_group = []
110+
note_lines_groups.append(current_group)
111+
else:
112+
current_group.append(stripped_line)
113+
return note_lines_groups
114+
115+
116+
def parse_note_line_groups(note_line_groups):
117+
voice_groups = []
118+
for group in note_line_groups:
119+
if len(group) == 0:
120+
continue
121+
freq_lines = parse_note_lines(group)
122+
voices = create_merged_frequency_lines(freq_lines)
123+
voice_groups.append(voices)
124+
return voice_groups
125+
126+
def parse_note_lines(note_lines):
127+
freq_lines = []
128+
for line in note_lines:
129+
octave = int(line[0])
130+
line_freqs = []
131+
for note in line[2:-1]:
132+
freq = int(notes[note][octave])
133+
line_freqs.append(freq)
134+
freq_lines.append(line_freqs)
135+
136+
return freq_lines
137+
138+
def create_merged_frequency_lines(freq_lines):
139+
voice_lines = []
140+
first_voice = freq_lines[0]
141+
voice_lines.append(first_voice)
142+
for line in freq_lines[1:]:
143+
for i in range(len(line)):
144+
if line[i] == 0:
145+
continue
146+
found_spot = False
147+
for voice in voice_lines:
148+
if voice[i] == 0 and line[i] != 0:
149+
voice[i] = line[i]
150+
found_spot = True
151+
break
152+
if not found_spot:
153+
new_voice = [0] * len(line)
154+
new_voice[i] = line[i]
155+
voice_lines.append(new_voice)
156+
157+
return voice_lines
158+
159+
notes = {
160+
"c" : [16.35, 32.7, 65.41, 130.81, 261.63, 523.25, 1046.5, 2093, 4186],
161+
"C" : [17.32, 34.65, 69.3, 138.59, 277.18, 554.37, 1108.73, 2217.46, 4434.92],
162+
"d" : [18.35, 36.71, 73.42, 146.83, 293.66, 587.33, 1174.66, 2349.32, 4698.64],
163+
"D" : [19.45, 38.89, 77.78, 155.56, 311.13, 622.25, 1244.51, 2489.02, 4978.03],
164+
"e" : [20.6, 41.2, 82.41, 164.81, 329.63, 659.25, 1318.51, 2637.02, 5274.04],
165+
"f" : [21.83, 43.65, 87.31, 174.61, 349.23, 698.46, 1396.91, 2793.83, 5587.65],
166+
"F" : [23.12, 46.25, 92.5, 185, 369.99, 739.99, 1479.98, 2959.96, 5919.91],
167+
"g" : [24.5, 49, 98, 196, 392, 783.99, 1567.98, 3135.96, 6271.93],
168+
"G" : [25.96, 51.91, 103.83, 207.65, 415.3, 830.61, 1661.22, 3322.44, 6644.88],
169+
"a" : [27.5, 55, 110, 220, 440, 880, 1760, 3520, 7040],
170+
"A" : [29.14, 58.27, 116.54, 233.08, 466.16, 932.33, 1864.66, 3729.31, 7458.62],
171+
"b" : [30.87, 61.74, 123.47, 246.94, 493.88, 987.77, 1975.53, 3951.07, 7902.13],
172+
"-" : [0, 0, 0, 0, 0, 0, 0, 0, 0], # Rest note
173+
">" : [-1, -1, -1, -1, -1, -1, -1, -1, -1] # Sustain note
174+
}

05_advanced/code/PWM_Music.py

Lines changed: 16 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,30 @@
11
from machine import Pin, PWM, Timer
22
import time
3-
from notes import *
3+
from MusicPlayer import *
44

5-
bpm = 110
5+
bpm = 140
66

7-
ms_pb = (1000*60)//bpm
8-
step = ms_pb // 16
7+
ms_pb = int((1000*60)//bpm)
8+
note_time = int(ms_pb // 4)
99

1010

11-
twinkle_notes = [G, G, P, D, D, P, E, E, DL, PL,
12-
C, C, P, B, B, P, A, A, GL, PL,
13-
D, D, P, C, C, P, B, B, AL, PL,
14-
G, G, P, D, D, P, E, E, DL, PL,
15-
C, C, P, B, B, P, A, A, GL]
16-
twinkle = parse_notes(twinkle_notes, step)
1711

18-
print("Done")
19-
timer0 = Timer(0)
12+
voice_groups = load_voice_groups_from_file("nothing_else_matters.lines")
13+
line_length = len(voice_groups[0][0])
14+
line_time = note_time * line_length
2015

16+
print("Number of voice groups:", len(voice_groups))
17+
print("Time per group:", line_time, "ms")
18+
for voice_group in voice_groups:
19+
print("Number of voices in group:", len(voice_group))
2120

21+
pins = [Pin(11, Pin.OUT), Pin(12, Pin.OUT), Pin(13, Pin.OUT), Pin(14, Pin.OUT)]
2222

23-
play_sequence(twinkle, timer0, Pin(13, Pin.OUT))
23+
player = MusicPlayer(voice_groups, note_time, pins)
24+
player.start()
2425

25-
mario_notes = ["E_44" , "E_44", "E_44", "C_44", "E_44", "G_44", "G_44", "C_44",
26-
"G_44", "E_44", "A_44", "B_44", "AS44", "A_44", "G_44", "E_44",
27-
"G_44", "A_44", "F_44", "G_44", "E_44", "C_44", "D_44", "B_44",
28-
"C_44", "G_44", "E_44", "A_44", "B_44", "AS44", "A_44", "G_44",
29-
"E_44", "G_44", "A_44", "F_44", "G_44", "E_44", "C_44", "D_44",
30-
"B_44", "G_44", "FS44", "F_44", "DS44", "E_44", "GS44", "A_44",
31-
"C_44", "A_44", "C_44", "D_44", "G_44", "FS44", "F_44", "DS44",
32-
"E_44",
33-
#"G_44"+"A_44"+"C_44", "G_44"+"A_44"+"C_44", "G_44"+"A_44"+"C_44",
34-
"G_44", "FS44", "F_44", "DS44", "E_44", "GS44", "A_44", "C_44",
35-
"A_44", "C_44", "D_44", "DS44", "D_44", "C_44", "C_44", "G_44",
36-
"E_44", "A_44", "B_44", "A_44", "GS44", "AS44", "GS44", "G_44", "F_44", "G_44"]
3726

38-
mario_notes2 = [E, E, P, E, P, C, E, P, G, PL, PL, G, PL, PL, C, PL,
39-
G, PL, E, PL, A, P, B, P, AS, A, P, G, E,
40-
G, A, P, F, G, P, E, P, C, D, B, PL,
41-
C, PL, G, PL, E, PL, A, P, B, P, AS, A, P, G,
42-
E, G, A, P, F, G, P, E, P, C, D,
43-
B, PL, PL, G, FS, F, DS, P, E, P, GS, A,
44-
C, P, A, C, D, PL, G, FS, F, DS, P,
45-
E, P, G, P, G, G, PL, PL, PL, G, FS, F, DS, P, E, P, GS, A, #G P G G should replace G's with GAC.
46-
C, P, A, C, D, PL, DS, PL, D, PL, C, PL, PL, PL, PL, C,
47-
G, PL, E, P, A, B, A, GS, AS, GS,
48-
"G_32", "F_42", GL]
49-
50-
mario_notes3 = [D, D, P, D, P, D, D, P, G, G, G, E, C, F, G, FS, F, E, C,
51-
E, F, D, E, C, E, F, D, G,
52-
E, C, F, G, FS, F, E, C,
53-
E, F, D, E, C, E, F, D, C,
54-
G, C, F, C, F, C, G, C,
55-
G, C, G, C, G, C, F, C,
56-
F, C, G, GS, AS, C, G, G,
57-
C, G, E, C, F, CS, F, C,
58-
E]
59-
60-
mario1 = parse_notes(mario_notes2, step)
61-
mario2 = parse_notes(mario_notes3, step)
62-
# play_sequence(mario1, timer0, Pin(13, Pin.OUT))
63-
# play_sequence(mario2, timer0, Pin(12, Pin.OUT))
64-
65-
66-
while True:
27+
while player.playing:
6728
pass
6829

69-
30+
print("Done playing song")

05_advanced/code/PWM_Music2.py

Lines changed: 0 additions & 24 deletions
This file was deleted.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import sys
2+
3+
def clean(file_path):
4+
cleaned_lines = []
5+
with open(file_path, "r") as f:
6+
lines = f.readlines()
7+
for line in lines:
8+
line = line.strip()
9+
line = line.strip("RH:")
10+
line = line.strip("LH:")
11+
if len(line) < 4:
12+
line = ""
13+
cleaned_lines.append(line)
14+
with open(file_path, "w") as f:
15+
for line in cleaned_lines:
16+
f.write(line + "\n")
17+
18+
19+
if __name__ == "__main__":
20+
if len(sys.argv) < 1:
21+
print("Usage: python clean_sheet_music.py <file_path>")
22+
sys.exit(1)
23+
file_path = sys.argv[1]
24+
clean(file_path)

05_advanced/code/jukebox.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import os
2+
from machine import Pin
3+
from MusicPlayer import *
4+
5+
class Jukebox:
6+
def __init__(self, directory, bpm=140):
7+
self.directory = directory
8+
self.songs = self.load_songs()
9+
self.pins = [Pin(11, Pin.OUT), Pin(12, Pin.OUT), Pin(13, Pin.OUT), Pin(14, Pin.OUT)]
10+
self.bpm = bpm
11+
self.ms_pb = int((1000*60)//self.bpm)
12+
self.note_time = int(self.ms_pb // 4)
13+
14+
15+
def load_songs(self):
16+
songs = []
17+
for filename in os.listdir(self.directory):
18+
if filename.endswith(".lines"):
19+
songs.append(filename)
20+
print("Loaded songs:", len(songs))
21+
return songs
22+
23+
def play_all(self):
24+
for song in self.songs:
25+
print(f"Playing {song}...")
26+
voice_groups = load_voice_groups_from_file(self.directory + "/" + song)
27+
player = MusicPlayer(voice_groups, self.note_time, self.pins)
28+
player.start()
29+
while player.playing:
30+
pass
31+
print(f"Finished playing {song}.")
32+
33+
34+
if __name__ == "__main__":
35+
jukebox = Jukebox("/")
36+
jukebox.play_all()

0 commit comments

Comments
 (0)