|
| 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