Examples¶
Basic¶
The most basic example of initiating an audio output and synth object.
1# pico_synth_sandbox - Basic Example
2# 2023 Cooper Dalrymple - me@dcdalrymple.com
3# GPL v3 License
4
5import time
6from pico_synth_sandbox.board import get_board
7from pico_synth_sandbox.audio import get_audio_driver
8from pico_synth_sandbox.synth import Synth
9from pico_synth_sandbox.voice.oscillator import Oscillator
10
11board = get_board()
12audio = get_audio_driver(board)
13synth = Synth(audio)
14synth.add_voice(Oscillator())
15
16while True:
17 synth.press(notenum=46)
18 time.sleep(1)
19 synth.release()
20 time.sleep(1)
Simple Synth¶
Use the display, touch keyboard, and encoder to control a basic synth object.
1# pico_synth_sandbox - Simple Synth Example
2# 2023 Cooper Dalrymple - me@dcdalrymple.com
3# GPL v3 License
4
5import pico_synth_sandbox.tasks
6from pico_synth_sandbox.board import get_board
7from pico_synth_sandbox.display import Display
8from pico_synth_sandbox.encoder import Encoder
9from pico_synth_sandbox.keyboard import get_keyboard_driver
10from pico_synth_sandbox.arpeggiator import Arpeggiator
11from pico_synth_sandbox.audio import get_audio_driver
12from pico_synth_sandbox.synth import Synth
13from pico_synth_sandbox.voice.oscillator import Oscillator
14
15board = get_board()
16
17display = Display(board)
18display.write("PicoSynthSandbox", (0,0))
19display.write("Loading...", (0,1))
20display.force_update()
21
22audio = get_audio_driver(board)
23synth = Synth(audio)
24synth.add_voices(Oscillator() for i in range(4))
25
26keyboard = get_keyboard_driver(board, max_voices = len(synth.voices))
27arpeggiator = Arpeggiator()
28keyboard.set_arpeggiator(arpeggiator)
29
30def voice_press(voice, notenum, velocity, keynum=None):
31 synth.press(voice, notenum, velocity)
32keyboard.set_voice_press(voice_press)
33
34def voice_release(voice, notenum, keynum=None):
35 synth.release(voice)
36keyboard.set_voice_release(voice_release)
37
38def key_press(keynum, notenum, velocity):
39 display.write("*", (keynum,1), 1)
40keyboard.set_key_press(key_press)
41
42def key_release(keynum, notenum):
43 display.write("_", (keynum,1), 1)
44keyboard.set_key_release(key_release)
45
46encoder = Encoder(board)
47def click():
48 arpeggiator.toggle()
49encoder.set_click(click)
50encoder.set_long_press(click)
51
52display.write("_"*len(keyboard.keys), (0,1))
53
54pico_synth_sandbox.tasks.run()
Filtered Synth¶
Use the display, touch keyboard, and encoder to control a more advanced synth object with amplitude envelope and filter settings. Filter uses both an envelope and low-frequency oscillator (“lfo”). Synth object must call update function in program loop for envelope and lfo to operate.
1# pico_synth_sandbox - Filtered Synth Example
2# 2023 Cooper Dalrymple - me@dcdalrymple.com
3# GPL v3 License
4
5import pico_synth_sandbox.tasks
6from pico_synth_sandbox.board import get_board
7from pico_synth_sandbox.display import Display
8from pico_synth_sandbox.encoder import Encoder
9from pico_synth_sandbox.audio import get_audio_driver
10from pico_synth_sandbox.synth import Synth
11from pico_synth_sandbox.voice.oscillator import Oscillator
12import pico_synth_sandbox.waveform as waveform
13from pico_synth_sandbox.keyboard import get_keyboard_driver
14from pico_synth_sandbox.arpeggiator import Arpeggiator
15
16board = get_board()
17
18display = Display(board)
19display.write("PicoSynthSandbox", (0,0))
20display.write("Loading...", (0,1))
21display.force_update()
22
23audio = get_audio_driver(board)
24synth = Synth(audio)
25synth.add_voices(Oscillator() for i in range(4))
26synth.set_waveform(waveform.get_saw())
27for voice in synth.voices:
28 voice.set_envelope(
29 attack_time=0.1,
30 decay_time=0.2,
31 release_time=1.0,
32 attack_level=1.0,
33 sustain_level=0.5
34 )
35 voice.set_filter(
36 type=Synth.FILTER_LPF,
37 frequency=1.0,
38 resonance=0.5,
39 envelope_attack_time=1.0,
40 envelope_release_time=1.0,
41 envelope_amount=0.05,
42 lfo_rate=0.5,
43 lfo_depth=0.05,
44 synth=synth
45 )
46
47keyboard = get_keyboard_driver(board, max_voices=len(synth.voices))
48arpeggiator = Arpeggiator()
49arpeggiator.set_octaves(1)
50arpeggiator.set_bpm(80)
51arpeggiator.set_steps(Arpeggiator.STEP_EIGHTH)
52arpeggiator.set_gate(0.5)
53keyboard.set_arpeggiator(arpeggiator)
54
55def press(voice, notenum, velocity, keynum=None):
56 synth.press(voice, notenum, velocity)
57keyboard.set_press(press)
58
59def release(voice, notenum, keynum=None):
60 synth.release(voice)
61keyboard.set_release(release)
62
63def key_press(keynum, notenum, velocity):
64 display.write("*", (keynum,1), 1)
65keyboard.set_key_press(key_press)
66
67def key_release(keynum, notenum):
68 display.write("_", (keynum,1), 1)
69keyboard.set_key_release(key_release)
70
71mod_value = 127
72encoder = Encoder(board)
73def update_filter():
74 synth.set_filter_frequency(float(mod_value) / 127.0)
75def increment():
76 global mod_value
77 if mod_value < 127:
78 mod_value += 1
79 update_filter()
80def decrement():
81 global mod_value
82 if mod_value > 0:
83 mod_value -= 1
84 update_filter()
85def click():
86 arpeggiator.toggle()
87encoder.set_increment(increment)
88encoder.set_decrement(decrement)
89encoder.set_click(click)
90encoder.set_long_press(click)
91
92display.write("_"*len(keyboard.keys), (0,1))
93
94pico_synth_sandbox.tasks.run()
Monophonic Synth¶
Control two different oscillators in sync with a single note from the touch keyboard. The encoder is used to increase or decrease the pitch of both oscillators by semitones.
1# pico_synth_sandbox - Monophonic Synth Example
2# 2023 Cooper Dalrymple - me@dcdalrymple.com
3# GPL v3 License
4
5import pico_synth_sandbox.tasks
6from pico_synth_sandbox.board import get_board
7from pico_synth_sandbox.display import Display
8from pico_synth_sandbox.encoder import Encoder
9from pico_synth_sandbox.keyboard import get_keyboard_driver
10from pico_synth_sandbox.audio import get_audio_driver
11from pico_synth_sandbox.synth import Synth
12from pico_synth_sandbox.voice.oscillator import Oscillator
13import pico_synth_sandbox.waveform as waveform
14
15board = get_board()
16
17display = Display(board)
18display.write("PicoSynthSandbox", (0,0))
19display.write("Loading...", (0,1))
20display.force_update()
21
22note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
23
24audio = get_audio_driver(board)
25synth = Synth(audio)
26
27osc1 = Oscillator()
28osc1.set_glide(1.0)
29osc1.set_waveform(waveform.get_sine())
30osc1.set_pan_rate(0.5)
31osc1.set_pan_depth(0.1)
32osc1.set_vibrato_rate(4.0)
33osc1.set_vibrato_depth(0.1)
34osc1.set_envelope(
35 attack_time=0.1,
36 attack_level=1.0,
37 decay_time=0.5,
38 sustain_level=0.25,
39 release_time=1.0
40)
41osc1.set_filter(
42 frequency=0.1,
43 resonance=0.5,
44 envelope_attack_time=0.5,
45 envelope_release_time=1.0,
46 envelope_amount=0.25,
47 lfo_rate=0.1,
48 lfo_depth=0.25,
49 synth=synth
50)
51osc1.set_coarse_tune(-1.0)
52synth.add_voice(osc1)
53
54osc2 = Oscillator()
55osc2.set_glide(0.25)
56osc2.set_waveform(waveform.get_sine())
57osc2.set_pan_rate(2.0)
58osc2.set_pan_depth(0.5)
59osc2.set_vibrato_rate(1.0)
60osc2.set_vibrato_depth(0.05)
61osc2.set_envelope(
62 attack_time=1.0,
63 attack_level=1.0,
64 decay_time=2.0,
65 sustain_level=0.0,
66 release_time=2.0
67)
68osc2.set_filter(
69 frequency=0.5,
70 resonance=0.1,
71 envelope_attack_time=2.0,
72 envelope_release_time=2.0,
73 envelope_amount=0.15,
74 synth=synth
75)
76osc2.set_coarse_tune(2.0)
77osc2.set_level(0.5)
78synth.add_voice(osc2)
79
80keyboard = get_keyboard_driver(board, max_voices=1)
81
82def press(voicenum, notenum, velocity, keynum=None):
83 for voice in synth.voices:
84 synth.press(voice, notenum, velocity)
85 display.write("{:02d} {}".format(notenum, note_names[notenum % 12]), (0,1), 6)
86keyboard.set_voice_press(press)
87
88def release(voicenum, notenum, keynum=None):
89 if not keyboard.has_notes():
90 synth.release()
91 display.write("", (0, 1), 6)
92keyboard.set_voice_release(release)
93
94tune = 0
95encoder = Encoder(board)
96def update_tuning():
97 global tune
98 osc1.set_coarse_tune(tune / 12.0)
99 osc2.set_coarse_tune(tune / 12.0 + 2.0)
100 display.write("{:02d}".format(tune), (6,1), 6, True)
101def increment():
102 global tune
103 if tune < 48:
104 tune += 1
105 update_tuning()
106def decrement():
107 global tune
108 if tune > -48:
109 tune -= 1
110 update_tuning()
111encoder.set_increment(increment)
112encoder.set_decrement(decrement)
113
114display.write("", (0,1), 6)
115update_tuning()
116
117pico_synth_sandbox.tasks.run()
Midi Controller¶
Use the touch keyboard and encoder to output midi note and control messages through the UART midi interface.
1# pico_synth_sandbox - Midi Controller Example
2# 2023 Cooper Dalrymple - me@dcdalrymple.com
3# GPL v3 License
4
5import pico_synth_sandbox.tasks
6from pico_synth_sandbox.board import get_board
7from pico_synth_sandbox.display import Display
8from pico_synth_sandbox.encoder import Encoder
9from pico_synth_sandbox.midi import Midi
10from pico_synth_sandbox.keyboard import get_keyboard_driver
11from pico_synth_sandbox.arpeggiator import Arpeggiator
12
13board = get_board()
14
15display = Display(board)
16display.write("PicoSynthSandbox", (0,0))
17display.write("Loading...", (0,1))
18display.force_update()
19
20mod_value = 0
21
22midi = Midi(board)
23midi.set_channel(1)
24midi.set_thru(True)
25
26keyboard = get_keyboard_driver(board, max_voices=0)
27arpeggiator = Arpeggiator()
28keyboard.set_arpeggiator(arpeggiator)
29
30def key_press(keynum, notenum, velocity):
31 midi.send_note_on(notenum, velocity)
32 display.write("*", (keynum,1), 1)
33keyboard.set_key_press(key_press)
34
35def key_release(keynum, notenum):
36 midi.send_note_off(notenum)
37 display.write("_", (keynum,1), 1)
38keyboard.set_key_release(key_release)
39
40encoder = Encoder(board)
41def increment():
42 global mod_value
43 if mod_value < 127:
44 mod_value += 1
45 midi.send_control_change(1, mod_value)
46 display.write(str(mod_value), (13,0), 3, True)
47def decrement():
48 global mod_value
49 if mod_value > 0:
50 mod_value -= 1
51 midi.send_control_change(1, mod_value)
52 display.write(str(mod_value), (13,0), 3, True)
53def click():
54 arpeggiator.toggle()
55encoder.set_increment(increment)
56encoder.set_decrement(decrement)
57encoder.set_click(click)
58
59def control_change(control, value):
60 if control == 64: # Sustain
61 keyboard.set_sustain(value)
62midi.set_control_change(control_change)
63
64display.write("CH:"+str(midi.get_channel()))
65display.write(str(mod_value), (13,0), 3, True)
66display.write("_"*len(keyboard.keys), (0,1))
67
68pico_synth_sandbox.tasks.run()
Drums¶
Use the display, touch keyboard, and encoder to play synthesized drum samples using the provided Drum classes. Encoder controls decay time of closed and open hi hat.
1# pico_synth_sandbox - Drums Example
2# 2023 Cooper Dalrymple - me@dcdalrymple.com
3# GPL v3 License
4
5import pico_synth_sandbox.tasks
6from pico_synth_sandbox.board import get_board
7from pico_synth_sandbox.display import Display
8from pico_synth_sandbox.encoder import Encoder
9from pico_synth_sandbox.audio import get_audio_driver
10from pico_synth_sandbox.synth import Synth
11from pico_synth_sandbox.voice.drum import Kick, Snare, ClosedHat, OpenHat
12from pico_synth_sandbox.keyboard import get_keyboard_driver
13from pico_synth_sandbox.arpeggiator import Arpeggiator
14
15board = get_board()
16
17display = Display(board)
18display.write("PicoSynthSandbox", (0,0))
19display.write("Loading...", (0,1))
20display.force_update()
21
22audio = get_audio_driver(board)
23synth = Synth(audio)
24synth.add_voice(Kick())
25synth.add_voice(Snare())
26# Keep local variable for changing envelope
27closed_hat = ClosedHat()
28open_hat = OpenHat()
29synth.add_voice(closed_hat)
30synth.add_voice(open_hat)
31
32keyboard = get_keyboard_driver(board, max_voices=1)
33arpeggiator = Arpeggiator()
34keyboard.set_arpeggiator(arpeggiator)
35
36def voice_press(voice, notenum, velocity, keynum=None):
37 voice = (notenum - keyboard.root) % len(synth.voices) # Ignore voice allocation
38 synth.press(voice, notenum, velocity)
39 if voice == 2: # Closed Hat
40 synth.release(open_hat, True) # Force release
41keyboard.set_voice_press(voice_press)
42
43def voice_release(voice, notenum, keynum=None):
44 voice = (notenum - keyboard.root) % len(synth.voices) # Ignore voice allocation
45 synth.release(voice)
46keyboard.set_voice_release(voice_release)
47
48def key_press(keynum, notenum, velocity):
49 display.write("*", (keynum,1), 1)
50keyboard.set_key_press(key_press)
51
52def key_release(keynum, notenum):
53 display.write("_", (keynum,1), 1)
54keyboard.set_key_release(key_release)
55
56mod_value = 64
57encoder = Encoder(board)
58def update_envelope():
59 closed_hat.set_time(float(mod_value) / 127.0)
60 open_hat.set_time(float(mod_value) / 127.0)
61def increment():
62 global mod_value
63 if mod_value < 127:
64 mod_value += 1
65 update_envelope()
66def decrement():
67 global mod_value
68 if mod_value > 0:
69 mod_value -= 1
70 update_envelope()
71def click():
72 arpeggiator.toggle()
73encoder.set_increment(increment)
74encoder.set_decrement(decrement)
75encoder.set_click(click)
76encoder.set_long_press(click)
77
78display.write("_"*len(keyboard.keys), (0,1))
79
80pico_synth_sandbox.tasks.run()
Drum Sequencer¶
Utilizes the Sequencer class to create a simple drum sequencer using the display, touch keyboard, encoder, and synthesized drum samples (similar to the “Drums” example). Encoder controls either the selected voice or beats per minute (aka “bpm”) and is switched by clicking the encoder. There is a total of 16 steps used by the sequencer, and the first 8 keys can be used to toggle whether a note is played on that particular step for the selected voice. The 12th key is used to switch between the lower and upper 8 steps. The 9th through 11th keys are not used in this example.
1# pico_synth_sandbox - Drum Sequencer Example
2# 2023 Cooper Dalrymple - me@dcdalrymple.com
3# GPL v3 License
4
5import pico_synth_sandbox.tasks
6from pico_synth_sandbox.board import get_board
7from pico_synth_sandbox.display import Display
8from pico_synth_sandbox.encoder import Encoder
9from pico_synth_sandbox.keyboard import get_keyboard_driver
10from pico_synth_sandbox.sequencer import Sequencer
11from pico_synth_sandbox.audio import get_audio_driver
12from pico_synth_sandbox.synth import Synth
13from pico_synth_sandbox.voice.drum import Kick, Snare, ClosedHat, OpenHat
14
15board = get_board()
16
17display = Display(board)
18display.write("PicoSynthSandbox", (0,0))
19display.write("Loading...", (0,1))
20display.force_update()
21display.set_cursor_enabled(True)
22display.set_cursor_blink(False)
23
24# Local parameters
25voice=0
26bpm=120
27alt_enc=False
28alt_key=False
29
30audio = get_audio_driver(board)
31synth = Synth(audio)
32synth.add_voices([
33 Kick(),
34 Snare(),
35 ClosedHat(),
36 OpenHat()
37])
38
39sequencer = Sequencer(
40 tracks=4,
41 bpm=120
42)
43def seq_step(position):
44 display.set_cursor_position(position, 1)
45def seq_press(notenum, velocity):
46 synth.press((notenum - 1) % len(synth.voices))
47def seq_release(notenum):
48 if (notenum - 1) % len(synth.voices) == 2: # Closed Hat
49 synth.release(3, True) # Force release Open Hat
50 synth.release((notenum - 1) % len(synth.voices))
51sequencer.set_step(seq_step)
52sequencer.set_press(seq_press)
53sequencer.set_release(seq_release)
54
55def update_display():
56 display.write(synth.voices[voice].__qualname__, (0, 0), 11)
57 display.write(">" if alt_enc else "<", (11,0), 1)
58 display.write(("^" if alt_key else "-") if len(keyboard.keys) < 16 else " ", (12,0), 1)
59 display.write(str(bpm), (13,0), 3, True)
60 line = ""
61 for i in range(sequencer.get_length()):
62 line += "*" if sequencer.has_note(i, voice) else "_"
63 display.write(line, (0,1))
64
65keyboard = get_keyboard_driver(board, max_voices=0)
66def key_press(keynum, notenum, velocity):
67 global sequencer
68
69 position = keynum
70 if len(keyboard.keys) < 16:
71 global alt_key
72 if keynum == 11:
73 alt_key = not alt_key
74 display.write("^" if alt_key else "-", (12,0), 1)
75 return
76 elif keynum < 8:
77 position = keynum + (8 if alt_key else 0)
78
79 position = position % sequencer.get_length()
80 if not sequencer.has_note(
81 position=position,
82 track=voice
83 ):
84 sequencer.set_note(
85 position=position,
86 notenum=voice+1,
87 velocity=1.0,
88 track=voice
89 )
90 display.write("*", (position,1), 1)
91 else:
92 sequencer.remove_note(
93 position=position,
94 track=voice
95 )
96 display.write("_", (position,1), 1)
97keyboard.set_key_press(key_press)
98
99def update_alt_enc():
100 global alt_enc
101 display.write(">" if alt_enc else "<", (11,0), 1)
102
103def update_bpm():
104 global bpm
105 sequencer.set_bpm(bpm)
106 display.write(str(bpm), (13,0), 3, True)
107
108def increment_track():
109 global alt_enc, voice
110 if alt_enc:
111 alt_enc = False
112 update_alt_enc()
113 voice = (voice + 1) % sequencer.get_tracks()
114 update_display()
115def decrement_track():
116 global alt_enc, voice
117 if alt_enc:
118 alt_enc = False
119 update_alt_enc()
120 voice = (voice - 1) % sequencer.get_tracks()
121 update_display()
122
123def increment_bpm():
124 global alt_enc, bpm
125 if not alt_enc:
126 alt_enc = True
127 update_alt_enc()
128 if bpm < 200:
129 bpm += 1
130 update_bpm()
131def decrement_bpm():
132 global alt_enc, bpm
133 if not alt_enc:
134 alt_enc = True
135 update_alt_enc()
136 if bpm > 50:
137 bpm -= 1
138 update_bpm()
139
140if board.num_encoders() == 1:
141
142 encoder = Encoder(board)
143
144 def increment():
145 global alt_enc
146 if not alt_enc:
147 increment_track()
148 else:
149 increment_bpm()
150 encoder.set_increment(increment)
151
152 def decrement():
153 global alt_enc
154 if not alt_enc:
155 decrement_track()
156 else:
157 decrement_bpm()
158 encoder.set_decrement(decrement)
159
160 def click():
161 global alt_enc
162 alt_enc = not alt_enc
163 update_alt_enc()
164 encoder.set_click(click)
165
166elif board.num_encoders() > 1:
167
168 encoder1 = Encoder(board, 0)
169 encoder1.set_increment(increment_track)
170 encoder1.set_decrement(decrement_track)
171
172 encoder2 = Encoder(board, 1)
173 encoder2.set_increment(increment_bpm)
174 encoder2.set_decrement(decrement_bpm)
175
176update_display()
177sequencer.enable()
178
179pico_synth_sandbox.tasks.run()
Microphone Example¶
Display the current microphone level as a horizontal bar graph. Set the record trigger and clip level using the encoder dial. Begin the record process by long pressing the encoder. The microphone will start recording when the input level is detected above the trigger level and will continue recording until the input level is detected below the clip level. The generated wave file will be saved to /samples/test.wav. Short click the encoder to reset the max level of the volume meter.
1# pico_synth_sandbox - Microphone Example
2# 2023 Cooper Dalrymple - me@dcdalrymple.com
3# GPL v3 License
4
5import time
6import pico_synth_sandbox.tasks
7from pico_synth_sandbox.tasks import Task
8from pico_synth_sandbox.board import get_board
9from pico_synth_sandbox.display import Display
10from pico_synth_sandbox.microphone import Microphone
11from pico_synth_sandbox.encoder import Encoder
12
13board = get_board()
14
15display = Display(board)
16display.enable_horizontal_graph()
17display.write("PicoSynthSandbox", (0,0))
18display.write("Loading...", (0,1))
19display.force_update()
20
21microphone = Microphone(board)
22
23trigger_level = 25
24trigger_level_step = 0.0001
25
26class MicrophoneLevel(Task):
27 def __init__(self, microphone, update=None):
28 self._microphone = microphone
29 self._max_level = 0.0
30 self._prev_max_level = 0.0
31 self._update = update
32 Task.__init__(self, update_frequency=5)
33 def set_update(self, callback):
34 self._update = callback
35 async def update(self):
36 self._level = self._microphone.get_level()
37 self._prev_max_level = self._max_level
38 self._max_level = max(self._level, self._max_level)
39 if self._update:
40 self._update(self._level, self._max_level, self._prev_max_level)
41 def get_max_level(self):
42 return self._max_level
43mic_level = MicrophoneLevel(microphone)
44
45def display_trigger():
46 global trigger_level, trigger_level_step
47 level_x = int(min(trigger_level*trigger_level_step/mic_level.get_max_level(), 1.0)*16) if mic_level.get_max_level() > 0.0 else 0
48 display.write(" " * level_x + chr(0xff), (0,0))
49
50def update_level(level, max_level, prev_max_level):
51 if max_level != prev_max_level:
52 display_trigger(False)
53 if max_level > 0.0:
54 display.write_horizontal_graph(level, 0.0, max_level, (0,1), 16)
55 else:
56 display.write("", (0,1), 16)
57mic_level.set_update(update_level)
58
59def record_trigger():
60 display.write("Recording", (0,0), 13)
61microphone.set_trigger(record_trigger)
62
63encoder = Encoder(board)
64
65def increment():
66 global trigger_level
67 if trigger_level < 100:
68 trigger_level += 1
69 display_trigger()
70encoder.set_increment(increment)
71
72def decrement():
73 global trigger_level
74 if trigger_level > 1:
75 trigger_level -= 1
76 display_trigger()
77encoder.set_decrement(decrement)
78
79def reset_max_level():
80 global max_level
81 max_level = 0.0
82encoder.set_click(reset_max_level)
83
84def start_record():
85 pico_synth_sandbox.tasks.pause()
86 display.clear()
87 display.write("Waiting")
88 display.force_update()
89 microphone.record(
90 name="test",
91 samples=4096,
92 trigger=trigger_level*trigger_level_step,
93 clip=trigger_level*trigger_level_step
94 )
95 display.write("Complete!")
96 display.force_update()
97 time.sleep(1)
98 display.clear()
99 display_trigger()
100 pico_synth_sandbox.tasks.resume()
101encoder.set_long_press(start_record)
102
103display.clear()
104display_trigger()
105
106pico_synth_sandbox.tasks.run()
Microphone FFT Example¶
Use the included Fast Fourier Transform utility to calculate and display the range of frequencies from the microphone input as a row of vertical bar graphs.
1# pico_synth_sandbox - Microphone FFT Example
2# 2023 Cooper Dalrymple - me@dcdalrymple.com
3# GPL v3 License
4
5import math
6import ulab.numpy as numpy
7from pico_synth_sandbox import fft
8from pico_synth_sandbox.board import get_board
9from pico_synth_sandbox.display import Display
10from pico_synth_sandbox.microphone import Microphone
11
12board = get_board()
13
14display = Display(board)
15display.enable_vertical_graph()
16
17microphone = Microphone(board)
18
19segments = 16
20spectro_range = 0.33
21min_val = None
22max_val = None
23
24while True:
25 fft_data = fft(microphone.get_buffer(256), log=True, dtype=numpy.uint16)
26
27 segment_len = int(math.floor(len(fft_data)*spectro_range/segments))
28 segment_data = [0 for i in range(segments)]
29 for i in range(segments):
30 segment_data[i] = numpy.max(fft_data[i*segment_len:(i+1)*segment_len])
31
32 if min_val is None:
33 min_val = numpy.min(segment_data)
34 else:
35 min_val = min(numpy.min(segment_data), min_val)
36
37 if max_val is None:
38 max_val = numpy.max(segment_data)
39 else:
40 max_val = max(numpy.max(segment_data), max_val)
41
42 for i in range(segments):
43 display.write_vertical_graph(
44 value = segment_data[i],
45 minimum = min_val,
46 maximum = max_val,
47 position = (i,0),
48 height = 2
49 )
50 display.force_update(reset_cursor=False)
Simple Sample Playback Example¶
Create a Sample voice and load the included /samples/hey.wav file. File is loaded during voice initialization and only one monophonic voice is available. The sample voice is set to not loop playback. Use the encoder click to toggle the arpeggiator.
1# pico_synth_sandbox - Simple Sample Playback Example
2# 2023 Cooper Dalrymple - me@dcdalrymple.com
3# GPL v3 License
4
5import pico_synth_sandbox.tasks
6from pico_synth_sandbox.board import get_board
7from pico_synth_sandbox.display import Display
8from pico_synth_sandbox.encoder import Encoder
9from pico_synth_sandbox.keyboard import get_keyboard_driver
10from pico_synth_sandbox.arpeggiator import Arpeggiator
11from pico_synth_sandbox.audio import get_audio_driver
12from pico_synth_sandbox.synth import Synth
13from pico_synth_sandbox.voice.sample import Sample
14
15board = get_board()
16
17display = Display(board)
18display.write("PicoSynthSandbox", (0,0))
19display.write("Loading...", (0,1))
20display.force_update()
21
22audio = get_audio_driver(board)
23audio.mute()
24synth = Synth(audio)
25synth.add_voice(Sample(loop=False, filepath="/samples/hey.wav"))
26
27keyboard = get_keyboard_driver(board, root=60, max_voices=len(synth.voices))
28arpeggiator = Arpeggiator()
29keyboard.set_arpeggiator(arpeggiator)
30
31def voice_press(voice, notenum, velocity, keynum=None):
32 synth.press(0, notenum, velocity)
33keyboard.set_voice_press(voice_press)
34
35def voice_release(voice, notenum, keynum=None):
36 if not synth.has_notes():
37 synth.release(0)
38keyboard.set_voice_release(voice_release)
39
40def key_press(keynum, notenum, velocity):
41 display.write("*", (keynum,1), 1)
42keyboard.set_key_press(key_press)
43
44def key_release(keynum, notenum):
45 display.write("_", (keynum,1), 1)
46keyboard.set_key_release(key_release)
47
48encoder = Encoder(board)
49def click():
50 arpeggiator.toggle()
51encoder.set_click(click)
52encoder.set_long_press(click)
53
54display.write("_"*len(keyboard.keys), (0,1))
55
56audio.unmute()
57
58pico_synth_sandbox.tasks.run()
Filtered Sample Example¶
Create 12 Sample voices using a single looping sample, the included /samples/hey.wav file. All of the available Oscillator parameters are defined to create a unique sound. Press the encoder to toggle between controlling the tune and filter frequency of the synthesizer.
1# pico_synth_sandbox - Filtered Sample Example
2# 2023 Cooper Dalrymple - me@dcdalrymple.com
3# GPL v3 License
4
5import random
6import pico_synth_sandbox.tasks
7from pico_synth_sandbox import fftfreq
8from pico_synth_sandbox.board import get_board
9from pico_synth_sandbox.display import Display
10from pico_synth_sandbox.encoder import Encoder
11from pico_synth_sandbox.keyboard import get_keyboard_driver
12from pico_synth_sandbox.audio import get_audio_driver
13from pico_synth_sandbox.synth import Synth
14from pico_synth_sandbox.voice.sample import Sample
15import pico_synth_sandbox.waveform as waveform
16
17board = get_board()
18
19display = Display(board)
20display.enable_horizontal_graph()
21display.write("PicoSynthSandbox", (0,0))
22display.write("Loading...", (0,1))
23display.force_update()
24
25audio = get_audio_driver(board)
26audio.mute()
27
28sample_data, sample_rate = waveform.load_from_file("/samples/hey.wav")
29root = fftfreq(
30 data=sample_data,
31 sample_rate=sample_rate
32)
33
34synth = Synth(audio)
35synth.add_voices(Sample(loop=True) for i in range(4))
36for voice in synth.voices:
37 voice.load(sample_data, sample_rate, root)
38 voice.set_envelope(
39 attack_time=0.1,
40 decay_time=0.2,
41 release_time=1.0,
42 attack_level=1.0,
43 sustain_level=0.5
44 )
45 voice.set_filter(
46 type=Synth.FILTER_LPF,
47 frequency=1.0,
48 resonance=0.5,
49 envelope_attack_time=1.0,
50 envelope_release_time=1.0,
51 envelope_amount=0.05,
52 lfo_rate=0.5,
53 lfo_depth=0.05,
54 synth=synth
55 )
56 voice.set_vibrato_depth(0.1)
57 voice.set_vibrato_rate(8.0)
58 voice.set_pan_rate(random.randint(0,80)/100.0+0.1)
59 voice.set_pan_depth(0.8)
60
61keyboard = get_keyboard_driver(board, max_voices=len(synth.voices))
62def press(voice, notenum, velocity, keynum=None):
63 synth.press(voice, notenum, velocity)
64keyboard.set_voice_press(press)
65def release(voice, notenum, keynum=None):
66 synth.release(voice)
67keyboard.set_voice_release(release)
68
69type = 0
70semitone = 0
71filter = 100
72
73def update_cursor():
74 global type
75 display.set_cursor_position(8 if type else 0, 0)
76update_cursor()
77
78def update_tune():
79 global semitone
80 for voice in synth.voices:
81 voice.set_coarse_tune(semitone / 12.0)
82 display.write_horizontal_graph(semitone, -48, 48, (0,1), 8)
83update_tune()
84
85def update_filter():
86 global filter
87 synth.set_filter_frequency(filter / 100.0)
88 display.write_horizontal_graph(filter, 0, 100, (8,1), 8)
89update_filter()
90
91def increment_semitone():
92 global type, semitone
93 if type != 0:
94 type = 0
95 update_cursor()
96 if semitone < 24:
97 semitone += 1
98 update_tune()
99def decrement_semitone():
100 global type, semitone
101 if type != 0:
102 type = 0
103 update_cursor()
104 if semitone > -24:
105 semitone -= 1
106 update_tune()
107
108def increment_filter():
109 global type, filter
110 if type != 1:
111 type = 1
112 update_cursor()
113 if filter < 100:
114 filter += 1
115 update_filter()
116def decrement_filter():
117 global type, filter
118 if type != 1:
119 type = 1
120 update_cursor()
121 if filter > 0:
122 filter -= 1
123 update_filter()
124
125if board.num_encoders() == 1:
126
127 encoder = Encoder(board)
128
129 def increment_encoder():
130 global type
131 if type == 0:
132 increment_semitone()
133 else:
134 increment_filter()
135 encoder.set_increment(increment_encoder)
136
137 def decrement_encoder():
138 global type
139 if type == 0:
140 increment_semitone()
141 else:
142 increment_filter()
143 encoder.set_decrement(decrement_encoder)
144
145 def toggle_encoder():
146 global type
147 type = 0 if type else 1
148 display.set_cursor_position(5 if type else 0, 0)
149 encoder.set_click(toggle_encoder)
150 encoder.set_long_press(toggle_encoder)
151
152elif board.num_encoders() > 1:
153
154 encoder1 = Encoder(board, 0)
155 encoder1.set_increment(increment_semitone)
156 encoder1.set_decrement(decrement_semitone)
157
158 encoder2 = Encoder(board, 1)
159 encoder2.set_increment(increment_filter)
160 encoder2.set_decrement(decrement_filter)
161
162
163display.write("Tune Filter ", position=(0,0))
164display.set_cursor_enabled(True)
165display.set_cursor_blink(True)
166display.force_update()
167
168audio.unmute()
169
170pico_synth_sandbox.tasks.run()
Sample Browser Example¶
Browse through every wav sample available in the /samples directory. 12 Sample voices are created to play through selected sample using touch keyboard. Press the encoder to toggle between controlling the tune and scrolling through the samples. Perform a long press of the encoder to load the selected sample. An * indicator in the upper right corner of the screen will indicate if the currently selected sample is loaded.
1# pico_synth_sandbox - Sample Browser Example
2# 2023 Cooper Dalrymple - me@dcdalrymple.com
3# GPL v3 License
4
5import gc, os
6import pico_synth_sandbox.tasks
7from pico_synth_sandbox import fftfreq
8from pico_synth_sandbox.board import get_board
9from pico_synth_sandbox.display import Display
10from pico_synth_sandbox.encoder import Encoder
11from pico_synth_sandbox.keyboard import get_keyboard_driver
12from pico_synth_sandbox.audio import get_audio_driver
13from pico_synth_sandbox.synth import Synth
14from pico_synth_sandbox.voice.sample import Sample
15import pico_synth_sandbox.waveform as waveform
16
17board = get_board()
18
19display = Display(board)
20display.enable_horizontal_graph()
21display.write("PicoSynthSandbox", (0,0))
22display.write("Loading...", (0,1))
23display.force_update()
24
25audio = get_audio_driver(board)
26synth = Synth(audio)
27synth.add_voices(Sample(loop=False) for i in range(4))
28for voice in synth.voices:
29 voice.set_envelope(
30 attack_time=0.05,
31 decay_time=0.1,
32 release_time=0.0,
33 attack_level=0.5,
34 sustain_level=0.5
35 )
36
37sample_data = None
38sample_rate = audio.get_sample_rate()
39sample_root = 440.0
40
41sample_files = list(filter(lambda x: x[-4:] == ".wav", os.listdir("/samples")))
42if not sample_files:
43 print("No samples available. Try running \"make samples --always-make\" in the library root directory.")
44 exit()
45
46keyboard = get_keyboard_driver(board, root=60, max_voices=len(synth.voices))
47def press(voice, notenum, velocity, keynum=None):
48 synth.press(voice, notenum, velocity)
49keyboard.set_voice_press(press)
50def release(voice, notenum, keynum=None):
51 synth.release(voice)
52keyboard.set_voice_release(release)
53
54type = 0
55semitone = 0
56sample_index = 0
57sample_index_loaded = -1
58
59def update_cursor():
60 global type
61 display.set_cursor_position(5 if type else 0, 0)
62
63def update_tune(write=True):
64 global semitone
65 for voice in synth.voices:
66 voice.set_coarse_tune(semitone / 12.0)
67 if write:
68 display.write_horizontal_graph(semitone, -24, 24, (0,1), 4)
69
70def update_sample():
71 global sample_index, sample_index_loaded
72 display.write(sample_files[sample_index][:-4], (5,1))
73 display.write("*" if sample_index == sample_index_loaded else " ", (15,0), 1)
74
75def increment_tune():
76 global type, semitone
77 if type != 0:
78 type = 0
79 update_cursor()
80 if semitone < 24:
81 semitone += 1
82 update_tune()
83def decrement_tune():
84 global type, semitone
85 if type != 0:
86 type = 0
87 update_cursor()
88 if semitone > -24:
89 semitone -= 1
90 update_tune()
91
92def increment_sample():
93 global type, sample_index, sample_files
94 if type != 1:
95 type = 1
96 update_cursor()
97 sample_index = (sample_index + 1) % len(sample_files)
98 update_sample()
99def decrement_sample():
100 global type, sample_index, sample_files
101 if type != 1:
102 type = 1
103 update_cursor()
104 sample_index = (sample_index - 1) % len(sample_files)
105 update_sample()
106
107def load_sample(write=True):
108 global semitone, sample_data, sample_rate, sample_root, sample_index, sample_index_loaded
109 if sample_index == sample_index_loaded:
110 return
111
112 pico_synth_sandbox.tasks.pause()
113 audio.mute()
114 if write:
115 display.write("Loading...", (5,1))
116 display.force_update()
117
118 for voice in synth.voices:
119 voice.unload()
120 del sample_data
121 gc.collect()
122
123 semitone = 0
124 update_tune(write)
125
126 sample_data, sample_rate = waveform.load_from_file("/samples/" + sample_files[sample_index], max_samples=8192)
127 sample_root = fftfreq(
128 data=sample_data,
129 sample_rate=sample_rate
130 )
131 for voice in synth.voices:
132 voice.load(sample_data, sample_rate, sample_root)
133
134 sample_index_loaded = sample_index
135 if write: update_sample()
136 audio.unmute()
137 pico_synth_sandbox.tasks.resume()
138
139if board.num_encoders() == 1:
140
141 encoder = Encoder(board)
142
143 def next():
144 global type
145 if type == 0:
146 increment_tune()
147 else:
148 increment_sample()
149 encoder.set_increment(next)
150
151 def previous():
152 global type
153 if type == 0:
154 increment_tune()
155 else:
156 increment_sample()
157 encoder.set_decrement(previous)
158
159 def toggle():
160 global type
161 type = 0 if type else 1
162 update_cursor()
163 encoder.set_click(toggle)
164
165 encoder.set_long_press(load_sample)
166
167elif board.num_encoders() > 1:
168
169 encoder1 = Encoder(board, 0)
170 encoder1.set_increment(increment_tune)
171 encoder1.set_decrement(decrement_tune)
172
173 encoder2 = Encoder(board, 1)
174 encoder2.set_increment(increment_sample)
175 encoder2.set_decrement(decrement_sample)
176 encoder2.set_click(load_sample)
177
178display.clear()
179display.write("Tune Sample", position=(0,0))
180display.set_cursor_enabled(True)
181display.set_cursor_blink(True)
182update_tune()
183update_sample()
184
185pico_synth_sandbox.tasks.run()
Microphone Sample Example¶
Use the microphone to record a sample into memory when the encoder is long pressed. The sample is then loaded into a simple Sample voice to play it back with the touch keyboard. You can control the pitch and filter of the voice using the encoder. Use a short click of the encoder to switch between the two options.
1# pico_synth_sandbox - Microphone Sample Example
2# 2023 Cooper Dalrymple - me@dcdalrymple.com
3# GPL v3 License
4
5import gc, time
6import pico_synth_sandbox.tasks
7from pico_synth_sandbox.tasks import Task
8from pico_synth_sandbox import fftfreq, normalize
9from pico_synth_sandbox.board import get_board
10from pico_synth_sandbox.display import Display
11from pico_synth_sandbox.encoder import Encoder
12from pico_synth_sandbox.audio import get_audio_driver
13from pico_synth_sandbox.synth import Synth
14from pico_synth_sandbox.voice.sample import Sample
15from pico_synth_sandbox.keyboard import get_keyboard_driver
16from pico_synth_sandbox.microphone import Microphone
17
18board = get_board()
19
20display = Display(board)
21display.enable_horizontal_graph()
22display.write("PicoSynthSandbox", (0,0))
23display.write("Loading...", (0,1))
24display.force_update()
25
26audio = get_audio_driver(board)
27synth = Synth(audio)
28voice = Sample(loop=False)
29synth.add_voice(voice)
30voice.set_envelope(
31 attack_time=0.05,
32 decay_time=0.1,
33 release_time=0.0,
34 attack_level=0.75,
35 sustain_level=0.75
36)
37voice.set_filter(
38 type=Synth.FILTER_LPF,
39 frequency=1.0,
40 resonance=0.5,
41 synth=synth
42)
43
44keyboard = get_keyboard_driver(board, root=60, max_voices=1)
45
46def voice_press(index, notenum, velocity, keynum=None):
47 synth.press(0, notenum, velocity)
48keyboard.set_voice_press(voice_press)
49
50def voice_release(index, notenum, keynum=None):
51 synth.release(0)
52keyboard.set_voice_release(voice_release)
53
54microphone = Microphone(board)
55
56class MicrophoneLevel(Task):
57 def __init__(self, microphone, update=None):
58 self._microphone = microphone
59 self._max_level = 0.0
60 self._update = update
61 Task.__init__(self, update_frequency=5)
62 async def update(self):
63 self._level = self._microphone.get_level()
64 self._max_level = max(self._level, self._max_level)
65 if self._update:
66 self._update(self._level, self._max_level)
67def update_level(level, max_level):
68 if max_level > 0.0:
69 display.write_horizontal_graph(level, 0.0, max_level, (12,1), 4)
70 else:
71 display.write("", (12,1), 4)
72mic_level = MicrophoneLevel(microphone, update_level)
73
74def trigger():
75 display.write("Recording")
76 display.force_update()
77microphone.set_trigger(trigger)
78
79type = 0
80semitone = 0
81filter = 100
82
83def update_tune(write=True):
84 global semitone
85 voice.set_coarse_tune(semitone / 12.0)
86 if write:
87 display.write_horizontal_graph(semitone, -24, 24, (0,1), 4)
88
89def update_filter(write=True):
90 global filter
91 voice.set_filter_frequency(filter / 100.0, synth)
92 if write:
93 display.write_horizontal_graph(filter, 0, 100, (5,1), 6)
94
95def update_cursor():
96 global type
97 display.set_cursor_position(5 if type else 0, 0)
98
99def reset_display():
100 display.clear()
101 display.write("Tune Filter Mic", (0,0))
102 update_tune()
103 update_filter()
104 update_cursor()
105 display.set_cursor_enabled(True)
106 display.set_cursor_blink(True)
107
108def start_record():
109 global semitone
110
111 pico_synth_sandbox.tasks.pause()
112 audio.mute()
113 display.set_cursor_enabled(False)
114 display.set_cursor_blink(False)
115 display.clear()
116
117 semitone = 0
118 update_tune(False)
119
120 voice.unload()
121 gc.collect()
122
123 display.write("Waiting")
124 display.force_update()
125
126 sample_data = microphone.read(
127 samples=4096,
128 trigger=0.01,
129 clip=0.001
130 )
131
132 display.write("Processing")
133 display.force_update()
134
135 # Normalize Volume
136 sample_data = normalize(sample_data)
137
138 # Calculate root frequency
139 sample_rate = Microphone.get_sample_rate()
140 sample_root = fftfreq(
141 data=sample_data,
142 sample_rate=sample_rate
143 )
144 voice.load(sample_data, sample_rate, sample_root)
145
146 display.clear()
147 display.write("Complete!")
148 display.force_update()
149 time.sleep(0.5)
150
151 reset_display()
152 audio.unmute()
153 pico_synth_sandbox.tasks.resume()
154
155def increment_semitone():
156 global type, semitone
157 if type != 0:
158 type = 0
159 update_cursor()
160 if semitone < 24:
161 semitone += 1
162 update_tune()
163def decrement_semitone():
164 global type, semitone
165 if type != 0:
166 type = 0
167 update_cursor()
168 if semitone > -24:
169 semitone -= 1
170 update_tune()
171
172def increment_filter():
173 global type, filter
174 if type != 1:
175 type = 1
176 update_cursor()
177 if filter < 100:
178 filter += 1
179 update_filter()
180def decrement_filter():
181 global type, filter
182 if type != 1:
183 type = 1
184 update_cursor()
185 if filter > 0:
186 filter -= 1
187 update_filter()
188
189if board.num_encoders() == 1:
190
191 encoder = Encoder(board)
192
193 def increment_encoder():
194 global type
195 if type == 0:
196 increment_semitone()
197 else:
198 increment_filter()
199 encoder.set_increment(increment_encoder)
200
201 def decrement_encoder():
202 global type
203 if type == 0:
204 increment_semitone()
205 else:
206 increment_filter()
207 encoder.set_decrement(decrement_encoder)
208
209 def toggle_encoder():
210 global type
211 type = 0 if type else 1
212 update_cursor()
213 encoder.set_click(toggle_encoder)
214
215 encoder.set_long_press(start_record)
216
217elif board.num_encoders() > 1:
218
219 encoder1 = Encoder(board, 0)
220 encoder1.set_increment(increment_semitone)
221 encoder1.set_decrement(decrement_semitone)
222 encoder1.set_long_press(start_record)
223
224 encoder2 = Encoder(board, 1)
225 encoder2.set_increment(increment_filter)
226 encoder2.set_decrement(decrement_filter)
227 encoder2.set_long_press(start_record)
228
229# Wait for microphone to initialize
230time.sleep(1)
231
232reset_display()
233
234pico_synth_sandbox.tasks.run()
Horizontal Bar Graph¶
Use custom characters to draw a horizontal bar on the lcd display. Useful for demonstrating parameter values. NOTE: vertical and horizontal bars cannot be displayed simultaneously, and display.enable_horizontal_graph must be called prior to display.write_horizontal_graph.
1# pico_synth_sandbox - Horizontal Bar Graph Example
2# 2023 Cooper Dalrymple - me@dcdalrymple.com
3# GPL v3 License
4
5import pico_synth_sandbox.tasks
6from pico_synth_sandbox.board import get_board
7from pico_synth_sandbox.display import Display
8from pico_synth_sandbox.encoder import Encoder
9
10board = get_board()
11
12display = Display(board)
13display.enable_horizontal_graph()
14
15value = 0
16def update_value():
17 global value
18 display.write(value, (0,0), 3, False)
19 display.write_horizontal_graph(
20 value=value,
21 minimum=0,
22 maximum=100,
23 position=(0,1),
24 width=16
25 )
26update_value()
27
28encoder = Encoder(board)
29def increment():
30 global value
31 if value < 100:
32 value += 1
33 update_value()
34encoder.set_increment(increment)
35def decrement():
36 global value
37 if value > 0:
38 value -= 1
39 update_value()
40encoder.set_decrement(decrement)
41
42pico_synth_sandbox.tasks.run()
Vertical Bar Graph¶
Use custom characters to draw a vertical bar on the lcd display. Useful for demonstrating levels. NOTE: vertical and horizontal bars cannot be displayed simultaneously, and display.enable_vertical_graph must be called prior to display.write_vertical_graph.
1# pico_synth_sandbox - Vertical Bar Graph Example
2# 2023 Cooper Dalrymple - me@dcdalrymple.com
3# GPL v3 License
4
5import pico_synth_sandbox.tasks
6from pico_synth_sandbox.board import get_board
7from pico_synth_sandbox.display import Display
8from pico_synth_sandbox.encoder import Encoder
9
10board = get_board()
11
12display = Display(board)
13display.enable_vertical_graph()
14
15value = 0
16def update_value():
17 global value
18 display.write(value, (13,0), 3, True)
19 display.write_vertical_graph(
20 value=value,
21 minimum=0,
22 maximum=100,
23 position=(0,0),
24 height=2
25 )
26update_value()
27
28encoder = Encoder(board)
29def increment():
30 global value
31 if value < 100:
32 value += 1
33 update_value()
34encoder.set_increment(increment)
35def decrement():
36 global value
37 if value > 0:
38 value -= 1
39 update_value()
40encoder.set_decrement(decrement)
41
42pico_synth_sandbox.tasks.run()