Skip to content

Commit 97967ec

Browse files
author
Kevin J Walters
committed
Demonstrating low-pass filter issue that occurs above nyquist rate.
1 parent 3d3d171 commit 97967ec

File tree

1 file changed

+201
-0
lines changed

1 file changed

+201
-0
lines changed

pico/synthio-filter-breakdown.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
### synthio-filter-breadkdown v1.0
2+
### Exploration of synthio filter sometimes producing white noise
3+
4+
### Tested with Pi Pico W (on EDU PICO) and 9.1.4
5+
6+
### copy this file to Cytron Maker Pi Pico as code.py
7+
8+
### MIT License
9+
10+
### Copyright (c) 2024 Kevin J. Walters
11+
12+
### Permission is hereby granted, free of charge, to any person obtaining a copy
13+
### of this software nd associated documentation files (the "Software"), to deal
14+
### in the Software without restriction, including without limitation the rights
15+
### to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16+
### copies of the Software, and to permit persons to whom the Software is
17+
### furnished to do so, subject to the following conditions:
18+
19+
### The above copyright notice and this permission notice shall be included in all
20+
### copies or substantial portions of the Software.
21+
22+
### THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23+
### IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24+
### FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25+
### AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26+
### LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27+
### OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28+
### SOFTWARE.
29+
30+
### low-pass filter iscussion in https://forums.adafruit.com/viewtopic.php?p=1034152
31+
32+
### Focus on C7 (note 84) - the Biquad filter turns to noise if low pass filter frequency
33+
### is above half the sample rate (the Nyquist rate)
34+
### Sample rate is set the same on Mixer and Sythenszier
35+
36+
37+
import os
38+
import time
39+
40+
import board
41+
##import analogio
42+
##import digitalio
43+
##import pwmio
44+
45+
import audiomixer
46+
import synthio
47+
import audiopwmio
48+
import ulab.numpy as np
49+
50+
import usb_midi
51+
import adafruit_midi
52+
from adafruit_midi.note_on import NoteOn
53+
from adafruit_midi.note_off import NoteOff
54+
from adafruit_midi.control_change import ControlChange
55+
from adafruit_midi import control_change_values
56+
57+
58+
debug = 2
59+
60+
61+
SAMPLE_RATE = 64_000
62+
63+
MIXER_BUFFER_SIZE = 2048
64+
WAVEFORM_PEAK = 28_000
65+
WAVEFORM_MAX = 2**15 - 1
66+
WAVEFORM_LEN = 8
67+
WAVEFORM_HALFLEN = WAVEFORM_LEN // 2
68+
MIDI_KEY_CHANNEL = 1
69+
### Wire protocol values
70+
MIDI_KEY_CHANNEL_WIRE = MIDI_KEY_CHANNEL - 1
71+
72+
if os.uname().machine.find("EDU PICO"):
73+
LEFT_AUDIO_PIN = board.GP20
74+
RIGHT_AUDIO_PIN = board.GP21
75+
elif os.uname().sysname == "rp2040":
76+
### Cytron Maker Pi Pico
77+
LEFT_AUDIO_PIN = board.GP18
78+
RIGHT_AUDIO_PIN = board.GP19
79+
else:
80+
### Custom RP2350B board
81+
LEFT_AUDIO_PIN = board.GP36
82+
RIGHT_AUDIO_PIN = board.GP37
83+
84+
85+
86+
87+
def d_print(level, *args, **kwargs):
88+
"""A simple conditional print for debugging based on global debug level."""
89+
if not isinstance(level, int):
90+
print(level, *args, **kwargs)
91+
elif debug >= level:
92+
print(*args, **kwargs)
93+
94+
95+
96+
waveform_saw = np.linspace(WAVEFORM_PEAK, 0 - WAVEFORM_PEAK, num=WAVEFORM_LEN,
97+
dtype=np.int16)
98+
midi_usb = adafruit_midi.MIDI(midi_in=usb_midi.ports[0],
99+
in_channel=MIDI_KEY_CHANNEL_WIRE)
100+
audio = audiopwmio.PWMAudioOut(LEFT_AUDIO_PIN, right_channel=RIGHT_AUDIO_PIN)
101+
mixer = audiomixer.Mixer(channel_count=1,
102+
sample_rate=SAMPLE_RATE,
103+
buffer_size=MIXER_BUFFER_SIZE)
104+
synth = synthio.Synthesizer(channel_count=1,
105+
sample_rate=SAMPLE_RATE)
106+
107+
audio.play(mixer)
108+
mixer.voice[0].play(synth)
109+
mixer.voice[0].level = 0.75
110+
111+
filter_freq_lo = 100 # filter lowest freq
112+
filter_freq_hi = 4500 # filter highest freq
113+
filter_note_offset_low = 58
114+
filter_note_offset_high = 62
115+
filter_res_lo = 0.1 # filter q lowest value
116+
filter_res_hi = 2.0 # filter q highest value
117+
118+
filter_note_offset = 37
119+
filter_res = 1.0 # current setting of filter
120+
amp_env_attack_time = 1.0
121+
amp_env_decay_time = 0.5
122+
amp_env_sustain = 0.8
123+
amp_env_release_time = 1.100
124+
125+
pressed = []
126+
oscs = []
127+
128+
129+
130+
### Simple range mapper, like Arduino map()
131+
def map_range(s, a1, a2, b1, b2):
132+
return b1 + ((s - a1) * (b2 - b1) / (a2 - a1))
133+
134+
135+
### pylint: disable=consider-using-in,too-many-branches
136+
def note_on(notenum, vel):
137+
138+
new_osc = []
139+
f_1 = synthio.midi_to_hz(notenum)
140+
filt_f_1 = synthio.midi_to_hz(notenum + filter_note_offset)
141+
filter_1 = synth.low_pass_filter(filt_f_1, filter_res)
142+
d_print(2, "FILTER FREQ", filt_f_1, "AM+S rate", SAMPLE_RATE)
143+
new_osc.append(synthio.Note(frequency=f_1,
144+
waveform=waveform_saw,
145+
amplitude=0.5,
146+
envelope=synthio.Envelope(attack_time=amp_env_attack_time,
147+
attack_level=1.0,
148+
decay_time=amp_env_decay_time,
149+
sustain_level=amp_env_sustain,
150+
release_time=amp_env_release_time),
151+
filter=filter_1
152+
))
153+
154+
oscs.clear()
155+
pressed.clear()
156+
157+
oscs.extend(new_osc)
158+
synth.press(oscs)
159+
pressed.append(notenum)
160+
161+
162+
def notes_off():
163+
164+
synth.release(oscs)
165+
oscs.clear()
166+
pressed.clear()
167+
168+
169+
last_note = None
170+
start_ns = time.monotonic_ns()
171+
while True:
172+
msg = midi_usb.receive()
173+
174+
if msg:
175+
if isinstance(msg, NoteOn) and msg.velocity != 0:
176+
d_print(2, "Note:", msg.note, "vel={:d}".format(msg.velocity))
177+
if last_note is not None:
178+
notes_off()
179+
note_on(msg.note, msg.velocity)
180+
last_note = msg.note
181+
182+
elif (isinstance(msg, NoteOff)
183+
or isinstance(msg, NoteOn) and msg.velocity == 0):
184+
d_print(2, "Note:", msg.note, "vel={:d}".format(msg.velocity))
185+
if msg.note in pressed: # only release note that's sounding
186+
notes_off()
187+
188+
elif isinstance(msg, ControlChange):
189+
d_print(2, "CC:", msg.control, "=", msg.value)
190+
if msg.control == control_change_values.CUTOFF_FREQUENCY: ### 74
191+
##filter_freq = map_range( msg.value, 0,127, filter_freq_lo, filter_freq_hi)
192+
filter_note_offset = map_range(msg.value,
193+
0, 127,
194+
filter_note_offset_low, filter_note_offset_high)
195+
elif msg.control == control_change_values.FILTER_RESONANCE: ### 71
196+
filter_res = map_range(msg.value, 0, 127, filter_res_lo, filter_res_hi)
197+
elif msg.control == control_change_values.RELEASE_TIME: ### 72
198+
amp_env_release_time = map_range(msg.value, 0, 127, 0.05, 3)
199+
200+
else:
201+
d_print(1, "MIDI MSG:", msg)

0 commit comments

Comments
 (0)