├── .gitignore ├── LICENSE ├── README.md ├── circuitpython ├── README.md ├── dronesynth │ ├── code.py │ ├── my_little_droney.py │ └── param_scaler.py ├── hwtest │ └── code.py ├── lib │ └── qtpy_synth │ │ ├── README.md │ │ ├── hardware.py │ │ ├── synthio_instrument.py │ │ └── winterbloom_smolmidi.py ├── requirements.txt ├── simpletouchsynth │ └── code.py └── wavesynth │ ├── code.py │ ├── wav │ ├── 303.WAV │ ├── ADDITIVE.WAV │ ├── AKVF_GRA.WAV │ ├── ANALOG_W.WAV │ ├── BRAIDS01.WAV │ ├── BRAIDS02.WAV │ ├── BRAIDS03.WAV │ ├── BRAIDS04.WAV │ ├── MICROW02.WAV │ ├── PLAITS01.WAV │ ├── PLAITS02.WAV │ └── PLAITS03.WAV │ └── wavesynth_display.py ├── docs ├── qtpy_synth_case1.jpg └── qtpy_synth_proto2a.jpg ├── enclosure └── qtpy_synth_case.stl └── schematics ├── gerbers └── qtpy_synth_proto2b_2023-08-05.zip ├── qtpy_synth_proto2a.brd ├── qtpy_synth_proto2a.sch ├── qtpy_synth_proto2b.brd ├── qtpy_synth_proto2b.sch ├── qtpy_synth_proto2b_bom.txt ├── qtpy_synth_proto2b_sch.pdf └── qtpy_synth_proto2b_sch.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore list for Eagle, a PCB layout tool 2 | 3 | # Backup files 4 | *.s#? 5 | *.b#? 6 | *.l#? 7 | *.b$? 8 | *.s$? 9 | *.l$? 10 | 11 | # Eagle project file 12 | # It contains a serial number and references to the file structure 13 | # on your computer. 14 | # comment the following line if you want to have your project file included. 15 | eagle.epf 16 | 17 | # Autorouter files 18 | *.pro 19 | *.job 20 | 21 | # CAM files 22 | *.$$$ 23 | *.cmp 24 | *.ly2 25 | *.l15 26 | *.sol 27 | *.plc 28 | *.stc 29 | *.sts 30 | *.crc 31 | *.crs 32 | 33 | *.dri 34 | *.drl 35 | *.gpi 36 | *.pls 37 | *.ger 38 | *.xln 39 | 40 | *.drd 41 | *.drd.* 42 | 43 | *.s#* 44 | *.b#* 45 | 46 | *.info 47 | 48 | *.eps 49 | 50 | # file locks introduced since 7.x 51 | *.lck 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tod Kurt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qtpy_synth 2 | 3 | A simple [QTPy RP2040](https://learn.adafruit.com/adafruit-qt-py-2040/overview)-based 4 | synth to experiment with [`synthio`](https://github.com/todbot/circuitpython-synthio-tricks). 5 | 6 | 7 | 8 | I sell on Tindie 9 | 10 | ### Features 11 | - Mono audio output circuit, converting PWM to audio, as per [RP2040 design guidelines](https://datasheets.raspberrypi.com/rp2040/hardware-design-with-rp2040.pdf#page=24) 12 | - Optoisolated MIDI Input via [MIDI TRS-A 3.5mm jack](https://www.perfectcircuit.com/make-noise-0-coast-midi-cable.html) 13 | - Two pots for controlling parameters 14 | - One switch for controlling parameters 15 | - Four capsense touch buttons for synth triggering 16 | - USB MIDI in/out of course too 17 | 18 | ### Software 19 | Some programs written specifically for this board: 20 | 21 | - [hwtest](./circuitpython/hwtest/code.py) - test out the hardware with a simple synth 22 | 23 | - [simpletouchsynth](./circuitpython/simpletouchsynth) - play with filters using touch sensors. Uses intial cut at a "qtpy_synth" hardware library. 24 | 25 | - [dronesynth](./circuitpython/dronesynth) - 8-oscillator drone synth 26 | 27 | - [wavesynth](./circuitpython/wavesynth) - larger general two-osc synth that can also do wave-mixing and wavetables. Has many rough edges but can sound great! 28 | - early video demo: 29 | 30 | [![Wavetable synth w/ CircuitPython synthio on QTPy RP2040](http://img.youtube.com/vi/4hgDi6MNfsI/0.jpg)](https://www.youtube.com/watch?v=4hgDi6MNfsI)] 31 | - another video demo: 32 | 33 | [![More Wavetable synth w/ CircuitPython synthio on QTPy RP2040](http://img.youtube.com/vi/80yjwxscnnA/0.jpg)](https://www.youtube.com/watch?v=80yjwxscnnA)] 34 | 35 | 36 | #### See also: 37 | - https://github.com/todbot/circuitpython-synthio-tricks 38 | - Contains [many other synthio examples](https://github.com/todbot/circuitpython-synthio-tricks/tree/main/examples) that can work with this synth with minimal changes 39 | 40 | ### Building it 41 | 42 | The [Tindie version is a partially-assembled PCB](https://www.tindie.com/products/todbot/qtpy_synth/). I sell on Tindie 43 | 44 | 45 | The completely assembled SMD sections of the board are: 46 | 47 | * audio output circuit 48 | * TRS MIDI input circuit 49 | 50 | The parts you need to add are: 51 | 52 | * [QTPy RP2040](https://www.adafruit.com/product/4900) 53 | * 0.91" [I2C OLED display](https://amzn.to/3KDmy73) (w/ pins in Gnd/Vcc/SCL/SDA order) 54 | * Two 10k potentiometers, 9mm ([example pots](https://amzn.to/3DYYJm5)) and [knobs](https://amzn.to/3QAkSyR) 55 | * One tact switch, 6mm ([example switch](https://amzn.to/47nJaSN)) 56 | * Headers and sockets to mount QTPy & display, to taste 57 | * [This male & female header kit looks pretty good](https://amzn.to/3qqbtiO), 58 | as I prefer to put female headers on the PCB and male headers on the QTPy and OLED display. 59 | You may need to trim down your headers to be the right size. 60 | * Feet. I like M2.5 nylon standoffs (e.g. [M2.5 standoff asst kit](https://amzn.to/45qVjFb)) 61 | but feel free to do what you like, including making an enclosure! 62 | * Or [3d-print a simple case](https://www.printables.com/model/757087-case-for-qtpy_synth-circuitpython-synthesizer) for it! I also like these [3d-printed knobs](https://www.thingiverse.com/thing:5759656) 63 | 64 | 65 | 66 | ### Pins used 67 | 68 | The board uses all of the QTPy RP2040 pins: 69 | 70 | * `board.A0` - left knob 71 | * `board.A1` - right knob 72 | * `board.TX` - middle button 73 | * `board.RX` - TRS UART MIDI in 74 | * `board.MOSI` - PWM Audio Out 75 | * `board.SCL` & `board.SDA` - I2C bus for OLED display 76 | * `board.A3`, `board.A2`, `board.MISO`, `board.SCK`, -- touch pads 1,2,3,4 77 | 78 | For more details see [`hwtest/code.py`](https://github.com/todbot/qtpy_synth/tree/main/circuitpython/hwtest/code.py). 79 | 80 | ### Schematic 81 | 82 | [](./schematics/qtpy_synth_proto2b_sch.pdf) 83 | -------------------------------------------------------------------------------- /circuitpython/README.md: -------------------------------------------------------------------------------- 1 | 2 | qtpy_synth circuitpython 3 | ======================== 4 | 5 | This is a collection of example synths to run on the qtpy_synth board. 6 | 7 | These examples include: 8 | 9 | - "[hwtest](./hwtest)" -- check that all hardware parts work 10 | - "[simpletouchsynth](./simpletouchsynth)" -- very simple single-patch synth with adjustable filter 11 | - "[wavesynth](./wavesynth)" -- wavetable synthesizer with GUI and wavetable selection 12 | 13 | 14 | ### Running the examples: 15 | 16 | To run these examples: 17 | 18 | 1. Copy the contents of the `lib` here directory to CIRCUITPY/lib 19 | 2. Copy everything in example synth's directory to the CIRCUITPY drive 20 | (e.g. `code.py` file and any other files) 21 | 3. Install external libraries. They are listed in `requirements.txt`. 22 | The easiest way to install them is with `circup` on the commandline: 23 | ```sh 24 | circup install -r requirements.txt 25 | ``` 26 | -------------------------------------------------------------------------------- /circuitpython/dronesynth/code.py: -------------------------------------------------------------------------------- 1 | # dronesynth_code.py -- drone synth for qtpy_synth 2 | # 25 Apr 2024 - @todbot / Tod Kurt 3 | # part of https://github.com/todbot/qtpy_synth 4 | # 5 | # Needed libraries to install: 6 | # circup install asyncio neopixel adafruit_debouncer adafruit_displayio_ssd1306 adafruit_display_text 7 | # Also install "qtpy_synth" directory in CIRCUITPY/lib 8 | # 9 | # How to use: 10 | # 11 | # - This synth has four pair of oscillators (8 total) 12 | # - Each pad controls an oscillator pad 13 | # - While holding a pad: 14 | # - left pot A adjusts oscillator pair center frequency 15 | # - right pot B adjusts oscillator frequency "spread" / "detune" 16 | # - middle tact button mutes/unmutes the oscillator pair 17 | # 18 | # 19 | 20 | import asyncio 21 | import time 22 | import random 23 | import synthio 24 | import usb_midi 25 | 26 | import displayio, terminalio, vectorio 27 | from adafruit_display_text import bitmap_label as label 28 | 29 | from qtpy_synth.hardware import Hardware 30 | 31 | from param_scaler import ParamScaler 32 | from my_little_droney import (MyLittleDroney, SynthConfig, 33 | get_freqs_by_knobs, note_to_knobval, knobval_to_note) 34 | 35 | import microcontroller 36 | microcontroller.cpu.frequency = 250_000_000 # overclock! vrrroomm 37 | 38 | num_pads = 4 39 | oscs_per_pad = 2 40 | note_offset = 12 41 | note_range = 60 42 | initial_vals = (note_to_knobval(36), note_to_knobval(48), 43 | note_to_knobval(36), note_to_knobval(60)) 44 | 45 | qts = Hardware() 46 | cfg = SynthConfig() 47 | 48 | droney = MyLittleDroney(qts.synth, cfg, num_pads, oscs_per_pad) #, initial_notes) 49 | 50 | pad_num = None # which pad is currently being touched 51 | voice_vals = [] # scaled knob vals per pad, index = pad number, val = [valA,valB] 52 | button_held = False 53 | button_held_time = 0 54 | 55 | # ----------------------------- 56 | 57 | # set up display with our chunks of info 58 | disp_group = displayio.Group() 59 | qts.display.root_group = disp_group 60 | labels_pos = ( (5,5), (45,5), 61 | (5,18), (45,18), 62 | (5,30), (45,30), 63 | (5,42), (45,42), 64 | (78,5), (78,18), (78,30), (78,42), 65 | (115,5) # dronesynth logo vertical 66 | ) 67 | disp_info = displayio.Group() 68 | for (x,y) in labels_pos: 69 | disp_info.append( label.Label(terminalio.FONT, text="--", x=x, y=y) ) 70 | disp_group.append(disp_info) 71 | disp_info[-1].text = "dronesynth" 72 | disp_info[-1].label_direction = "UPR" 73 | 74 | def display_update(): 75 | for i in range(num_pads): 76 | fstr = disp_info[i*2+0].text 77 | dstr = disp_info[i*2+1].text 78 | fstr_new = "%.1f" % knobval_to_note(voice_vals[i][0], note_offset, note_range) 79 | dstr_new = "%.1f" % voice_vals[i][1] 80 | if fstr_new != fstr: 81 | disp_info[i*2+0].text = fstr_new 82 | if dstr_new != dstr: 83 | disp_info[i*2+1].text = dstr_new 84 | onoff = "on" if droney.voices[i][0].amplitude else "--" 85 | if disp_info[8+i].text != onoff: 86 | disp_info[8+i].text = onoff 87 | 88 | 89 | # -------------------------------------------------------- 90 | 91 | # get initial knob vals for the scalers 92 | (knobA_val, knobB_val) = [v/256 for v in qts.read_pots()] 93 | 94 | # set up the inital state of the voices 95 | for i in range(num_pads): 96 | vs = [initial_vals[i], 0] 97 | voice_vals.append(vs) 98 | 99 | # set up default freqs in droney 100 | freqs = get_freqs_by_knobs(vs[0],vs[1], note_offset, note_range) 101 | print("freqs:", freqs) 102 | droney.set_voice_freqs(i, freqs) 103 | 104 | # start with only two of the voices sounding 105 | droney.toggle_voice_mute(2) 106 | droney.toggle_voice_mute(3) 107 | 108 | scalerA = ParamScaler(voice_vals[0][0], knobA_val) 109 | scalerB = ParamScaler(voice_vals[0][1], knobB_val) 110 | 111 | globalA_val = knobA_val 112 | globalB_val = knobB_val 113 | 114 | display_update() 115 | 116 | debug = False 117 | def dbg(*args,**kwargs): 118 | if debug: print(*args,**kwargs) 119 | 120 | while True: 121 | time.sleep(0.001) 122 | 123 | #droney.update() 124 | 125 | (knobA_val, knobB_val) = [v/256 for v in qts.read_pots()] 126 | 127 | # handle held touch pad 128 | if pad_num is not None: 129 | 130 | valA = scalerA.update(knobA_val) #, debug=True) 131 | valB = scalerB.update(knobB_val) 132 | 133 | dbg("knobA:%.1f knobB:%.1f valA:%.1f valB:%.1f" % 134 | (knobA_val, knobB_val, valA, valB)) 135 | 136 | freqs = get_freqs_by_knobs(valA, valB, note_offset, note_range) 137 | droney.set_voice_freqs(pad_num, freqs) 138 | 139 | voice_vals[touch.key_number] = [valA,valB] 140 | display_update() 141 | 142 | else: 143 | globalA_val = scalerA.update(knobA_val) 144 | globalB_val = scalerB.update(knobB_val) # , debug=True) 145 | #dbg("global: %1.f %1.f" %(globalA_val, globalB_val)) 146 | #droney.set_pitch_lfo_amount(knobB_val/255) 147 | #note_offset = (globalA_val/255) * 24 148 | f = 20 + (globalA_val/255) * 3000 149 | droney.set_filter(f,None) 150 | 151 | # handle pad press 152 | if touches := qts.check_touch(): 153 | for touch in touches: 154 | 155 | if touch.pressed: 156 | pad_num = touch.key_number 157 | # restore vals for this pad 158 | lastA, lastB = voice_vals[pad_num] 159 | scalerA.reset(lastA, knobA_val) 160 | scalerB.reset(lastB, knobB_val) 161 | 162 | if touch.released: 163 | pad_num = None 164 | # restore the global vals 165 | scalerA.reset(globalA_val, knobA_val) 166 | scalerB.reset(globalB_val, knobB_val) 167 | 168 | # handle tact button press 169 | if key := qts.check_key(): 170 | if key.pressed: 171 | print("button!") 172 | button_held = True 173 | button_held_time = time.monotonic() 174 | droney.print_freqs() 175 | if pad_num is not None: # pressing a pad toggles voice 176 | droney.toggle_voice_mute(pad_num) 177 | 178 | 179 | if key.released: 180 | button_held = False 181 | 182 | # handle button held 183 | button_time_delta = time.monotonic() - button_held_time 184 | if button_held and button_time_delta > 1: 185 | dbg("drone freqs") 186 | for i,vs in enumerate(voice_vals): 187 | dbg("%d: " % i, end='') 188 | for v in vs: 189 | dbg("%.2f, " % v, end='') 190 | dbg() 191 | 192 | 193 | # if button is held, convert the notes to pad1 note 194 | converge_valA, converge_valB = voice_vals[0] 195 | cff = 0.05 196 | for i in range(1,num_pads): 197 | voice_vals[i][0] = cff*converge_valA + (1-cff)*voice_vals[i][0] 198 | freqs = get_freqs_by_knobs(voice_vals[i][0], voice_vals[i][1], note_offset, note_range) 199 | droney.set_voice_freqs(i, freqs) 200 | display_update() 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /circuitpython/dronesynth/my_little_droney.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2024 Tod Kurt 2 | # SPDX-License-Identifier: MIT 3 | """ 4 | `my_little_droney` 5 | ================================================================================ 6 | 7 | `MyLittleDroney` is a multi-voice drone synth with multiple oscillators per voice. 8 | 9 | Part of qtpy_synth. 10 | 11 | """ 12 | 13 | import math 14 | import random 15 | import synthio 16 | import ulab.numpy as np 17 | 18 | 19 | # set up some default synth parameters 20 | wave_size = 256 21 | wave_amp = 20000 22 | wave_saw = np.linspace(wave_amp,-wave_amp, num=wave_size, dtype=np.int16) 23 | wave_sin = np.array(np.sin(np.linspace(0, 2*np.pi, wave_size, endpoint=False)) * wave_amp, dtype=np.int16) 24 | wave_squ = np.concatenate((np.ones(wave_size // 2, dtype=np.int16) * wave_amp, 25 | np.ones(wave_size // 2, dtype=np.int16) * -wave_amp)) 26 | # default squ is too clippy, should be 3dB down or so? 27 | 28 | def hz_to_midi(f): 29 | """ Convert Hz to MIDI note number float, synthio doesn't provide this """ 30 | return 12 * (math.log(f,2) - math.log(440,2)) + 69 31 | 32 | def note_to_knobval(n, note_offset=12, note_range=60): 33 | """ Turn a midi note number into a 'knobval' (0-255 float) """ 34 | return (n-note_offset) * 255 / note_range 35 | 36 | def knobval_to_note(v, note_offset=12, note_range=60): 37 | """ Turn a 'knobval' (0-255 float) into a float MIDI note number """ 38 | return ((v/255) * note_range) + note_offset 39 | 40 | def get_freqs_by_knobs(valA,valB, note_offset=12, note_range=60): 41 | """ Create a list of frequencies based on two 0-255 inputs """ 42 | n = knobval_to_note(valA, note_offset, note_range) 43 | d = 0.0001 + (valB / 255) * 2 44 | f1 = synthio.midi_to_hz( n ) 45 | f2 = synthio.midi_to_hz( n + d ) 46 | return (f1, f2) 47 | 48 | def get_wave(wave_type): 49 | if wave_type=='sin': return wave_sin 50 | if wave_type=='squ': return wave_squ 51 | if wave_type=='saw': return wave_saw 52 | 53 | filter_types = ['lpf', 'hpf', 'bpf'] 54 | 55 | def make_filter(synth,cfg): 56 | freq = cfg.filter_f + cfg.filter_fmod 57 | if cfg.filter_type == 'lpf': 58 | filter = synth.low_pass_filter(freq, cfg.filter_q) 59 | elif cfg.filter_type == 'hpf': 60 | filter = synth.high_pass_filter(freq, cfg.filter_q) 61 | elif cfg.filter_type == 'bpf': 62 | filter = synth.band_pass_filter(freq, cfg.filter_q) 63 | else: 64 | print("unknown filter type", cfg.filter_type) 65 | return filter 66 | 67 | class SynthConfig(): 68 | def __init__(self): 69 | self.filter_type = 'lpf' 70 | self.filter_f = 2000 71 | self.filter_q = 1.1 72 | self.filter_fmod = 500 73 | self.wave_type = 'saw' # 'sin' or 'saw' or 'squ' 74 | self.pitch_mod_rate = 0.1 75 | self.pitch_mod_scale = 0.01 76 | 77 | class MyLittleDroney(): 78 | """ 79 | Drone Synth 80 | """ 81 | def __init__(self, synth, synth_config, num_voices, oscs_per_voice): 82 | self.voices = [] 83 | self.synth = synth 84 | self.cfg = synth_config 85 | for i in range(num_voices): 86 | oscs = [] 87 | freqs = get_freqs_by_knobs(127,0) # fake values 88 | wave = get_wave(self.cfg.wave_type) 89 | 90 | for j in range(oscs_per_voice): 91 | f = freqs[j] 92 | pitch_lfo = synthio.LFO(rate=0.1, scale=0.01, phase_offset=random.uniform(0,1)) 93 | osc = synthio.Note(frequency=f, waveform=wave, 94 | bend=pitch_lfo, 95 | filter = make_filter(self.synth,self.cfg)) 96 | synth.press(osc) 97 | oscs.append(osc) 98 | self.voices.append(oscs) 99 | 100 | # def update(self): 101 | # for voice in self.voices: 102 | # for osc in voice: 103 | # osc.filter = make_filter(self.synth,self.cfg) 104 | 105 | def set_voice_freqs(self,n,freqs): 106 | for i,osc in enumerate(self.voices[n]): 107 | osc.frequency = freqs[i] 108 | 109 | def set_voice_notes(self,n,notes): 110 | freqs = [synthio.midi_to_hz(n) for n in notes] 111 | for i,osc in enumerate(self.voices[n]): 112 | osc.frequency = freqs[i] 113 | 114 | def set_voice_level(self,n,level): 115 | for osc in self.voices[n]: 116 | osc.amplitude = level 117 | 118 | def toggle_voice_mute(self,n): 119 | # fixme: what about other levels than 0,1 120 | for osc in self.voices[n]: 121 | osc.amplitude = 0 if osc.amplitude > 0 else 1 122 | 123 | def print_freqs(self): 124 | for i in range(len(self.voices)): 125 | print("%d: " % i, end='') 126 | oscs = self.voices[i] 127 | for osc in oscs: 128 | print("%.2f, " % osc.frequency, end='') 129 | print() 130 | 131 | def set_pitch_lfo_amount(self,n): 132 | for voice in self.voices: 133 | for osc in voice: 134 | osc.bend.scale = n 135 | 136 | def set_filter(self,f,q): 137 | self.cfg.filter_f = f 138 | self.cfg.filter_q = q if q else self.cfg.filter_q 139 | for voice in self.voices: 140 | for osc in voice: 141 | osc.filter = make_filter(self.synth, self.cfg) 142 | 143 | 144 | -------------------------------------------------------------------------------- /circuitpython/dronesynth/param_scaler.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2024 Tod Kurt 2 | # SPDX-License-Identifier: MIT 3 | """ 4 | `param_scaler` 5 | ================================================================================ 6 | 7 | `ParamScaler` attempts to solve the "knob pickup" problem when a control's 8 | position does not match the Param position. 9 | 10 | The scaler will increase/decrease its internal value relative to the change 11 | of the incoming knob position and the amount of "runway" remaining on the 12 | value. Once the knob reaches its max or min position, the value will 13 | move in sync with the knob. The value will always decrease/increase 14 | in the same direction as the knob. 15 | This mirrors how the Deluge synth's "SCALE" mode works. 16 | 17 | Part of synth_tools. 18 | 19 | Example: 20 | 21 | scaler = ParamScaler(127, pot.value) # make a scaler 22 | while True: 23 | 24 | knob_pos = pot.value # get "real" position of pot 25 | 26 | # scales 'val' in direciton of 'knob_val' turning 27 | val_to_use = scaler.update(knob_pos) 28 | 29 | if button: 30 | # loading a new value, means need to update scaler 31 | val = get_new_val() 32 | scaler.reset(val, knob_pos) 33 | 34 | """ 35 | 36 | #from micropython import const 37 | 38 | #knob_min, knob_max = const(0), const(255) 39 | #val_min, val_max = const(0), const(255) 40 | 41 | knob_min, knob_max = 0, 255 42 | val_min, val_max = 0, 255 43 | 44 | class ParamScaler: 45 | def __init__(self, val_start, knob_pos): 46 | """ Make a ParamScaler, val and knob_pos range from 0-255, floating point """ 47 | self.val = val_start 48 | self.last_knob_pos = knob_pos 49 | self.knob_match = False 50 | self.dead_zone = 1 51 | 52 | def reset(self, val=None, knob_pos=None): 53 | """ Resets current val and knob_pos (e.g. for loading a new controlled parameter """ 54 | if val is not None: 55 | self.val = val 56 | if knob_pos is not None: 57 | self.last_knob_pos = knob_pos 58 | self.knob_match = False 59 | 60 | def update(self, knob_pos, debug=False): 61 | """ 62 | Call whenever knob_position changes. 63 | Returns a value scale in direction of knob turn 64 | """ 65 | if debug: 66 | print("knob_pos:%.1f last:%3d val:%.1f" % (knob_pos, self.last_knob_pos, self.val)) 67 | 68 | knob_pos = min(max(knob_pos,knob_min),knob_max) # ensure within bounds 69 | 70 | knob_delta = knob_pos - self.last_knob_pos 71 | 72 | if abs(knob_delta) < self.dead_zone: 73 | return self.val 74 | 75 | self.last_knob_pos = knob_pos 76 | 77 | knob_delta_pos = (knob_max - knob_pos) 78 | knob_delta_neg = (knob_pos - knob_min) 79 | 80 | val_delta_pos = (val_max - self.val) 81 | val_delta_neg = (self.val - val_min) 82 | 83 | if debug: 84 | print("deltas: %.1f %.1f %.1f, %.1f %.1f" % 85 | (knob_delta, knob_delta_pos,knob_delta_neg, val_delta_pos,val_delta_neg)) 86 | 87 | val_change = 0 88 | if knob_delta > self.dead_zone and knob_delta_pos != 0: 89 | val_change = (knob_delta * val_delta_pos) / knob_delta_pos 90 | elif knob_delta < -self.dead_zone and knob_delta_neg != 0: 91 | val_change = (knob_delta * val_delta_neg) / knob_delta_neg 92 | 93 | if debug: print("val_change:%.1f" % val_change) 94 | self.val = min(max(self.val + val_change, val_min), val_max) 95 | return self.val 96 | 97 | -------------------------------------------------------------------------------- /circuitpython/hwtest/code.py: -------------------------------------------------------------------------------- 1 | # qtpy_synth_hwtest_code.py -- test hardware of qtpy_synth board 2 | # 27 Jun 2023 - @todbot / Tod Kurt 3 | # 4 | # Functionality: 5 | # - touch pads 1,2,3,4 trigger synth notes (defined in 'midi_notes') 6 | # - middle button triggers random synth notes 7 | # - left knob controls filter cutoff 8 | # - right knob controls filter resonance 9 | # - sending MIDI via USB or TRS UART MIDI will print MIDI messages to REPL and play simple tones 10 | # 11 | # Libaries needed: 12 | # - asyncio 13 | # - adafruit_debouncer 14 | # - adafruit_displayio_ssd1306 15 | # - adafruit_display_text 16 | # - winterbloom_smolmidi (included, but from https://github.com/wntrblm/Winterbloom_SmolMIDI) 17 | # Install them all with: 18 | # circup install asyncio adafruit_debouncer adafruit_displayio_ssd1306 adafruit_display_text 19 | # 20 | # 21 | import asyncio 22 | import time, random 23 | import board 24 | # knobs 25 | import analogio, keypad 26 | # audio 27 | import audiopwmio, audiomixer, synthio 28 | import ulab.numpy as np 29 | # display 30 | import displayio, terminalio 31 | import adafruit_displayio_ssd1306 32 | from adafruit_display_text import bitmap_label as label 33 | # touch 34 | import touchio 35 | from adafruit_debouncer import Debouncer 36 | # midi 37 | import busio 38 | import usb_midi 39 | import qtpy_synth.winterbloom_smolmidi as smolmidi 40 | 41 | midi_notes = (33, 45, 52, 57) 42 | filter_freq = 4000 43 | filter_resonance = 1.2 44 | 45 | displayio.release_displays() 46 | 47 | # set up our knobs and button 48 | knobA = analogio.AnalogIn(board.A0) 49 | knobB = analogio.AnalogIn(board.A1) 50 | keys = keypad.Keys( pins=(board.TX,), value_when_pressed=False ) 51 | 52 | # set up touch pins using a debouncer 53 | touch_pins = (board.A3, board.A2, board.MISO, board.SCK) 54 | touchins = [] 55 | touchs = [] 56 | for pin in touch_pins: 57 | touchin = touchio.TouchIn(pin) 58 | touchins.append(touchin) 59 | touchs.append( Debouncer(touchin) ) 60 | 61 | midi_uart = busio.UART(rx=board.RX, baudrate=31250, timeout=0.001) 62 | i2c = busio.I2C(scl=board.SCL, sda=board.SDA, frequency=1_000_000 ) 63 | 64 | dw,dh = 128, 64 65 | display_bus = displayio.I2CDisplay(i2c, device_address=0x3c ) 66 | display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=dw, height=dh, rotation=180) 67 | 68 | # set up the synth->audio system 69 | audio = audiopwmio.PWMAudioOut(board.MOSI) 70 | mixer = audiomixer.Mixer(voice_count=1, sample_rate=28000, channel_count=1, 71 | bits_per_sample=16, samples_signed=True, 72 | buffer_size=4096) # need a big buffer when screen updated 73 | synth = synthio.Synthesizer(sample_rate=28000) 74 | audio.play(mixer) 75 | mixer.voice[0].level = 0.75 # turn down the volume a bit since this can get loud 76 | mixer.voice[0].play(synth) 77 | 78 | # set up MIDI inputs 79 | midi_usb_in = smolmidi.MidiIn(usb_midi.ports[0]) 80 | midi_uart_in = smolmidi.MidiIn(midi_uart) 81 | 82 | # set up the synth 83 | wave_saw = np.linspace(30000,-30000, num=256, dtype=np.int16) # default squ is too clippy 84 | amp_env = synthio.Envelope(sustain_level=0.8, release_time=0.4, attack_time=0.001) 85 | synth.envelope = amp_env 86 | 87 | # set up info to be displayed 88 | maingroup = displayio.Group() 89 | display.show(maingroup) 90 | text1 = label.Label(terminalio.FONT, text="helloworld...", x=0, y=10) 91 | text2 = label.Label(terminalio.FONT, text="@todbot", x=0, y=25) 92 | text3 = label.Label(terminalio.FONT, text="hwtest. press!", x=0, y=50) 93 | for t in (text1, text2, text3): 94 | maingroup.append(t) 95 | 96 | touch_notes = [None] * 4 97 | sw_pressed = False 98 | 99 | def check_touch(): 100 | for i in range(len(touchs)): 101 | touch = touchs[i] 102 | touch.update() 103 | if touch.rose: 104 | print("touch press",i) 105 | f = synthio.midi_to_hz(midi_notes[i]) 106 | filt = synth.low_pass_filter(filter_freq, filter_resonance) 107 | n = synthio.Note( frequency=f, waveform=wave_saw, filter=filt) 108 | synth.press( n ) 109 | touch_notes[i] = n 110 | if touch.fell: 111 | print("touch release", i) 112 | synth.release( touch_notes[i] ) 113 | 114 | async def debug_printer(): 115 | while True: 116 | text1.text = "K:%3d %3d S:%d" % (knobA.value//255, knobB.value//255, sw_pressed) 117 | text2.text = "T:" + ''.join(["%3d " % v for v in ( 118 | touchins[0].raw_value//16, touchins[1].raw_value//16, 119 | touchins[2].raw_value//16, touchins[3].raw_value//16)]) 120 | print(text1.text, text2.text, midi_usb_in._error_count) 121 | await asyncio.sleep(0.3) 122 | 123 | async def input_handler(): 124 | global sw_pressed 125 | global filter_freq, filter_resonance 126 | 127 | note = None 128 | 129 | while True: 130 | filter_freq = knobA.value/65535 * 8000 + 100 # range 100-8100 131 | filter_resonance = knobB.value/65535 * 3 + 0.2 # range 0.2-3.2 132 | 133 | for n in touch_notes: # real-time adjustment of filter 134 | if n: 135 | n.filter = synth.low_pass_filter(filter_freq, filter_resonance) 136 | 137 | check_touch() 138 | 139 | if key := keys.events.get(): 140 | if key.released: 141 | sw_pressed = False 142 | synth.release( note ) 143 | print("button release!") 144 | if key.pressed: 145 | sw_pressed = True 146 | f = synthio.midi_to_hz(random.randint(32,60)) 147 | note = synthio.Note(frequency=f, waveform=wave_saw) # , filter=filter) 148 | synth.press(note) 149 | print("button press!") 150 | await asyncio.sleep(0.01) 151 | 152 | 153 | async def midi_handler(): 154 | while True: 155 | while msg := midi_uart_in.receive() or midi_usb_in.receive(): 156 | if msg.type == smolmidi.NOTE_ON: 157 | chan = msg.channel 158 | note = msg.data[0] 159 | vel = msg.data[1] 160 | print('NoteOn: Ch: {} Note: {} Vel:{}'.format(chan, note, vel)) 161 | synth.press( note ) 162 | elif msg.type == smolmidi.NOTE_OFF: 163 | chan = msg.channel 164 | note = msg.data[0] 165 | vel = msg.data[1] 166 | print('NoteOff: Ch: {} Note: {} Vel:{}'.format(chan, note, vel)) 167 | synth.release( note ) 168 | elif msg.type == smolmidi.PITCH_BEND: 169 | chan = msg.channel 170 | pbval = (msg.data[1] << 7) | msg.data[0] 171 | print("PitchBend: Ch: {} Bend: {}".format(chan, pbval)) 172 | elif msg.type == smolmidi.CC: 173 | chan = msg.channel 174 | ccnum = msg.data[0] 175 | ccval = msg.data[1] 176 | print('CC Ch: {}, Num: {} Val: {}'.format(chan, ccnum, ccval)) 177 | elif msg.type == smolmidi.CHANNEL_PRESSURE: 178 | chan = msg.channel 179 | press_val = msg.data[0] 180 | print('Ch Pressure: Ch: {}, Val: {}'.format(chan, press_val)) 181 | elif msg.type == smolmidi.SYSTEM_RESET: 182 | print('System reset') 183 | else: 184 | print("unknown message:",msg) 185 | await asyncio.sleep(0) 186 | 187 | 188 | # main coroutine 189 | async def main(): # Don't forget the async! 190 | task1 = asyncio.create_task(debug_printer()) 191 | task2 = asyncio.create_task(input_handler()) 192 | task3 = asyncio.create_task(midi_handler()) 193 | await asyncio.gather(task1,task2,task3) 194 | 195 | print("hello qtpy_synth hwtest") 196 | asyncio.run(main()) 197 | -------------------------------------------------------------------------------- /circuitpython/lib/qtpy_synth/README.md: -------------------------------------------------------------------------------- 1 | 2 | This directory contains libraries for use with qtpy_synth. 3 | For actual applications, see the other directories 4 | -------------------------------------------------------------------------------- /circuitpython/lib/qtpy_synth/hardware.py: -------------------------------------------------------------------------------- 1 | # qtpy_synth.hardware.py -- hardware defines and setup for qtpy_synth board 2 | # 22 Jul 2023 - @todbot / Tod Kurt 3 | # part of https://github.com/todbot/qtpy_synth 4 | # 5 | # libraries needed: 6 | # circup install neopixel, adafruit_debouncer, adafruit_displayio_ssd1306 7 | # 8 | # UI fixme: 9 | # knob "pickup" vs knob "catchup" (maybe done in app instead) 10 | 11 | import board, busio 12 | import analogio, keypad 13 | import touchio 14 | from adafruit_debouncer import Debouncer 15 | import neopixel 16 | import audiopwmio, audiomixer 17 | import synthio 18 | import displayio 19 | import adafruit_displayio_ssd1306 20 | 21 | SAMPLE_RATE = 25600 # lets try powers of two 22 | #SAMPLE_RATE = 22050 # lets try powers of two 23 | MIXER_BUFFER_SIZE = 4096 24 | DW,DH = 128, 64 # display width/height 25 | 26 | class Hardware(): 27 | def __init__(self): 28 | 29 | self.led = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.1) 30 | self.keys = keypad.Keys( pins=(board.TX,), value_when_pressed=False ) 31 | self._knobA = analogio.AnalogIn(board.A0) 32 | self._knobB = analogio.AnalogIn(board.A1) 33 | self.knobA = self._knobA.value 34 | self.knobB = self._knobB.value 35 | 36 | self.touchins = [] # for raw_value 37 | self.touches = [] # for debouncer 38 | for pin in (board.A3, board.A2, board.MISO, board.SCK): 39 | touchin = touchio.TouchIn(pin) 40 | touchin.threshold = int(touchin.threshold * 1.1) # noise protec 41 | self.touchins.append(touchin) 42 | self.touches.append(Debouncer(touchin)) 43 | 44 | self.midi_uart = busio.UART(rx=board.RX, baudrate=31250, timeout=0.001) 45 | 46 | displayio.release_displays() 47 | i2c = busio.I2C(scl=board.SCL, sda=board.SDA, frequency=400_000 ) 48 | display_bus = displayio.I2CDisplay(i2c, device_address=0x3c ) 49 | self.display = adafruit_displayio_ssd1306.SSD1306(display_bus, 50 | width=DW, height=DH, 51 | rotation=180) 52 | 53 | # now do audio setup so we have minimal audible glitches 54 | self.audio = audiopwmio.PWMAudioOut(board.MOSI) 55 | self.mixer = audiomixer.Mixer(sample_rate=SAMPLE_RATE, 56 | voice_count=1, channel_count=1, 57 | bits_per_sample=16, samples_signed=True, 58 | buffer_size=MIXER_BUFFER_SIZE) 59 | self.synth = synthio.Synthesizer(sample_rate=SAMPLE_RATE) 60 | self.audio.play(self.mixer) 61 | self.mixer.voice[0].play(self.synth) 62 | 63 | def set_volume(self,v): 64 | self.mixer.voice[0].level = v 65 | 66 | def check_key(self): 67 | return self.keys.events.get() 68 | 69 | def read_pots(self): 70 | """Read the knobs, filter out their noise """ 71 | filt = 0.5 72 | 73 | # avg_cnt = 5 74 | # knobA_vals = [self.knobA] * avg_cnt 75 | # knobB_vals = [self.knobB] * avg_cnt 76 | # for i in range(avg_cnt): 77 | # knobA_vals[i] = self._knobA.value 78 | # knobB_vals[i] = self._knobB.value 79 | 80 | # self.knobA = filt * self.knobA + (1-filt)*(sum(knobA_vals)/avg_cnt) # filter noise 81 | # self.knobB = filt * self.knobB + (1-filt)*(sum(knobB_vals)/avg_cnt) # filter noise 82 | 83 | self.knobA = filt * self.knobA + (1-filt)*(self._knobA.value) # filter noise 84 | self.knobB = filt * self.knobB + (1-filt)*(self._knobB.value) # filter noise 85 | return (int(self.knobA), int(self.knobB)) 86 | 87 | def check_touch(self): 88 | """Check the four touch inputs, return keypad-like Events""" 89 | events = [] 90 | for i in 0,1,2,3: 91 | touch = self.touches[i] 92 | touch.update() 93 | if touch.rose: 94 | events.append(keypad.Event(i,True)) 95 | elif touch.fell: 96 | events.append(keypad.Event(i,False)) 97 | return events 98 | 99 | def check_touch_hold(self, hold_func): 100 | for i in 0,1,2,3: 101 | if self.touches[i].value: # pressed 102 | v = self.touchins[i].raw_value - self.touchins[i].threshold 103 | hold_func(i, v) 104 | -------------------------------------------------------------------------------- /circuitpython/lib/qtpy_synth/synthio_instrument.py: -------------------------------------------------------------------------------- 1 | 2 | import time 3 | import synthio 4 | from collections import namedtuple 5 | from micropython import const 6 | import ulab.numpy as np 7 | try: 8 | import adafruit_wave 9 | except: 10 | print("synthio_instrment: no WAV import available") 11 | 12 | # mix between values a and b, works with numpy arrays too, t ranges 0-1 13 | def lerp(a, b, t): return (1-t)*a + t*b 14 | 15 | 16 | class Waves: 17 | """ 18 | Generate waveforms for either oscillator or LFO use 19 | """ 20 | def make_waveform(waveid, size=512, volume=30000): 21 | waveid = waveid.upper() 22 | if waveid=='SIN' or waveid=='SINE': 23 | return Waves.sine(size,volume) 24 | elif waveid=='SQU' or waveid=='SQUARE': 25 | return Waves.square(size,volume) 26 | elif waveid=='SAW': 27 | return Waves.saw(size,volume) 28 | elif waveid=='TRI' or waveid == 'TRIANGLE': 29 | return Waves.triangle(size, -volume, volume) 30 | elif waveid=='SIL' or waveid=='SILENCE': 31 | return Waves.silence(size) 32 | elif waveid=='NZE' or waveid=='NOISE': 33 | return Waves.noise(size,volume) 34 | else: 35 | print("unknown wave type", waveid) 36 | 37 | def sine(size, volume): 38 | return np.array(np.sin(np.linspace(0, 2*np.pi, size, endpoint=False)) * volume, dtype=np.int16) 39 | 40 | def square(size, volume): 41 | return np.concatenate((np.ones(size//2, dtype=np.int16) * volume, 42 | np.ones(size//2, dtype=np.int16) * -volume)) 43 | 44 | def triangle(size, min_vol, max_vol): 45 | return np.concatenate((np.linspace(min_vol, max_vol, num=size//2, dtype=np.int16), 46 | np.linspace(max_vol, min_vol, num=size//2, dtype=np.int16))) 47 | 48 | def saw(size, volume): 49 | return Waves.saw_down(size,volume) 50 | 51 | def saw_down(size, volume): 52 | return np.linspace(volume, -volume, num=size, dtype=np.int16) 53 | 54 | def saw_up(size, volume): 55 | return np.linspace(-volume, volume, num=size, dtype=np.int16) 56 | 57 | def silence(size): 58 | return np.zeros(size, dtype=np.int16) 59 | 60 | def noise(size,volume): 61 | # tbd 62 | pass 63 | 64 | def from_list( vals ): 65 | print("Waves.from_list: vals=",vals) 66 | return np.array( [int(v) for v in vals], dtype=np.int16 ) 67 | 68 | def lfo_ramp_up_pos(): 69 | return np.array( (0,32767), dtype=np.int16) 70 | 71 | def lfo_ramp_down_pos(): 72 | return np.array( (32767,0), dtype=np.int16) 73 | 74 | def lfo_triangle_pos(): 75 | return np.array( (0, 32767, 0), dtype=np.int16) 76 | 77 | def lfo_triangle(): 78 | return np.array( (0, 32767, 0, -32767), dtype=np.int16) 79 | 80 | def wav(filepath, size=256, pos=0): 81 | with adafruit_wave.open(filepath) as w: 82 | if w.getsampwidth() != 2 or w.getnchannels() != 1: 83 | raise ValueError("unsupported format") 84 | #n = w.getnframes() if size==0 else size 85 | n = size 86 | w.setpos(pos) 87 | return np.frombuffer(w.readframes(n), dtype=np.int16) 88 | 89 | def wav_info(filepath): 90 | with adafruit_wave.open(filepath) as w: 91 | return (w.getnframes(), w.getnchannels(), w.getsampwidth()) 92 | 93 | 94 | class Wavetable: 95 | """ 96 | A 'waveform' for synthio.Note that uses a wavetable with a scannable 97 | wave position. A wavetable is a collection of harmonically-related 98 | single-cycle waveforms. Often the waveforms are 256 samples long and 99 | the wavetable containing 64 waves. The wavetable oscillator lets the 100 | user pick which of those 64 waves to use, usually allowing one to mix 101 | between two waves. 102 | 103 | Some example wavetables usable by this classs: https://waveeditonline.com/ 104 | 105 | In this implementation, you select a wave position (wave_pos) that can be 106 | fractional, and the fractional part allows for mixing of the waves 107 | at wave_pos and wave_pos+1. 108 | """ 109 | 110 | def __init__(self, filepath, size=256, in_memory=False): 111 | self.filepath = filepath 112 | """Sample size of each wave in the table""" 113 | self.size = size 114 | self.w = adafruit_wave.open(filepath) 115 | if self.w.getsampwidth() != 2 or self.w.getnchannels() != 1: 116 | raise ValueError("unsupported WAV format") 117 | self.wav = None 118 | if in_memory: # load entire WAV into RAM 119 | self.wav = np.frombuffer(self.w.readframes(self.w.getnframes()), dtype=np.int16) 120 | self.samp_posA = -1 121 | 122 | """How many waves in this wavetable""" 123 | self.num_waves = self.w.getnframes() / self.size 124 | """ The waveform to be used by synthio.Note """ 125 | self.waveform = Waves.silence(size) # makes a buffer for us to lerp into 126 | self.set_wave_pos(0) 127 | 128 | def set_wave_pos(self,wave_pos): 129 | """ 130 | wave_pos integer part of specifies which wave from 0-num_waves, 131 | and fractional part specifies mix between wave and wave next to it 132 | (e.g. wave_pos=15.66 chooses 1/3 of waveform 15 and 2/3 of waveform 16) 133 | """ 134 | wave_pos = min(max(wave_pos, 0), self.num_waves-1) # constrain 135 | self.wave_pos = wave_pos 136 | 137 | samp_posA = int(wave_pos) * self.size 138 | samp_posB = int(wave_pos+1) * self.size 139 | #print("samp_posA", samp_posA, self.samp_posA, wave_pos) 140 | if samp_posA != self.samp_posA: # avoid needless computation 141 | if self.wav: # if we've loaded the entire wavetable into RAM 142 | waveformA = self.wav[samp_posA : samp_posA + self.size] # slice 143 | waveformB = self.wav[samp_posB : samp_posB + self.size] 144 | else: 145 | self.w.setpos(samp_posA) 146 | waveformA = np.frombuffer(self.w.readframes(self.size), dtype=np.int16) 147 | self.w.setpos(samp_posB) 148 | waveformB = np.frombuffer(self.w.readframes(self.size), dtype=np.int16) 149 | 150 | self.samp_posA = samp_posA # save 151 | self.waveformA = waveformA 152 | self.waveformB = waveformB 153 | 154 | # fractional position between a wave A & B 155 | wave_pos_frac = wave_pos - int(wave_pos) 156 | # mix waveforms A & B and copy result into waveform used by synthio 157 | self.waveform[:] = lerp(self.waveformA, self.waveformB, wave_pos_frac) 158 | 159 | def deinit(self): 160 | self.w.close() 161 | 162 | 163 | class LFOParams: 164 | """ 165 | """ 166 | def __init__(self, rate=None, scale=None, offset=None, once=False, waveform=None): 167 | self.rate = rate 168 | self.scale = scale 169 | self.offset = offset 170 | self.once = once 171 | self.waveform = waveform 172 | 173 | def make_lfo(self): 174 | return synthio.LFO(rate=self.rate, once=self.once, 175 | scale=self.scale, offset=self.offset, 176 | waveform=self.waveform) 177 | 178 | class EnvParams(): 179 | """ 180 | """ 181 | def __init__(self, attack_time=0.1, decay_time=0.01, release_time=0.2, attack_level=0.8, sustain_level=0.8): 182 | self.attack_time = attack_time 183 | self.decay_time = decay_time 184 | self.release_time = release_time 185 | self.attack_level = attack_level 186 | self.sustain_level = sustain_level 187 | 188 | def make_env(self): 189 | return synthio.Envelope(attack_time = self.attack_time, 190 | decay_time = self.decay_time, 191 | release_time = self.release_time, 192 | attack_level = self.attack_level, 193 | sustain_level = self.sustain_level) 194 | 195 | class FiltType: 196 | """ """ 197 | LP = const(0) 198 | HP = const(1) 199 | BP = const(2) 200 | def str(t): 201 | if t==LP: return 'LP' 202 | elif t==HP: return 'HP' 203 | elif t==BP: return 'BP' 204 | return 'UN' 205 | 206 | class WaveType: 207 | OSC = const(0) 208 | WTB = const(1) 209 | def str(t): 210 | if t==WTB: return 'wtb' 211 | return 'osc' 212 | def from_str(s): 213 | if s=='wtb': return WTB 214 | return OSC 215 | 216 | class Patch: 217 | """ Patch is a serializable data structure for the Instrument's settings 218 | FIXME: patches should have names too, tod 219 | """ 220 | def __init__(self, name, wave_type=WaveType.OSC, wave='SAW', detune=1.01, 221 | filt_type=FiltType.LP, filt_f=8000, filt_q=1.2, 222 | filt_env_params=None, amp_env_params=None): 223 | self.name = name 224 | self.wave_type = wave_type # or 'osc' or 'wav' or 'wtb' 225 | self.wave = wave 226 | self.waveB = None 227 | self.wave_mix = 0.0 # 0 = wave, 1 = waveB 228 | self.wave_mix_lfo_amount = 3 229 | self.wave_mix_lfo_rate = 0.5 230 | self.wave_dir = '/wav' 231 | self.detune = detune 232 | self.filt_type = filt_type # allowed values: 233 | self.filt_f = filt_f 234 | self.filt_q = filt_q 235 | self.filt_env_params = filt_env_params or EnvParams() 236 | self.amp_env_params = amp_env_params or EnvParams() 237 | 238 | def wave_select(self): 239 | """Construct a 'wave_select' string from patch parts""" 240 | waveB_str = "/"+self.waveB if self.waveB else "" 241 | waveA_str = self.wave.replace('.WAV','') # if it's a wavetable 242 | wave_select = WaveType.str(self.wave_type) + ":" + waveA_str + waveB_str 243 | return wave_select 244 | 245 | def set_by_wave_select(self, wave_select): 246 | """ Parse a 'wave_select' string and set patch from it""" 247 | wave_type_str, oscs = wave_select.split(':') 248 | self.wave_type = WaveType.from_str(wave_type_str) 249 | self.wave, *waveB = oscs.split('/') # wave contains wavetable filename if wave_type=='wtb' 250 | self.waveB = waveB[0] if waveB and len(waveB) else None # can this be shorter? 251 | 252 | def __repr__(self): 253 | return "Patch('%s','%s')" % (self.name,self.wave_select()) 254 | 255 | 256 | # a very simple instrument 257 | class Instrument(): 258 | 259 | def __init__(self, synth, patch=None): 260 | self.synth = synth 261 | self.patch = patch or Patch('init') 262 | self.voices = {} # keys = midi note, vals = oscs 263 | 264 | def update(self): 265 | for v in self.voices: 266 | print("note:",v) 267 | 268 | def note_on(self, midi_note, midi_vel=127): 269 | # FIXME: deal with multiple note_ons of same note 270 | f = synthio.midi_to_hz(midi_note) 271 | amp_env = self.patch.amp_env_params.make_env() 272 | voice = synthio.Note( frequency=f, envelope=amp_env ) 273 | self.voices[midi_note] = voice 274 | self.synth.press( voice ) 275 | 276 | def note_off(self, midi_note, midi_vel=0): 277 | voice = self.voices.get(midi_note, None) 278 | if voice: 279 | self.synth.release(voice) 280 | self.voices.pop(midi_note) # FIXME: need to run filter after release cycle 281 | 282 | # 283 | class MonoOsc(Instrument): 284 | def __init__(self, synth, patch): 285 | super().__init__(synth) 286 | self.load_patch(patch) 287 | 288 | 289 | # 290 | class WavePolyTwoOsc(Instrument): 291 | #Voice = namedtuple("Voice", "osc1 osc2 filt_env amp_env") # idea: 292 | """ 293 | This is a two-oscillator per voice subtractive synth patch 294 | with a low-pass filter w/ filter envelope and an amplitude envelope 295 | """ 296 | def __init__(self, synth, patch): 297 | super().__init__(synth) 298 | self.load_patch(patch) 299 | 300 | def load_patch(self, patch): 301 | """Loads patch specifics from passed-in Patch object. 302 | ##### FIXME: no it doesnt: Will reload patch if patch is not specified. """ 303 | self.patch = patch # self.patch or patch 304 | print("PolyTwoOsc.load_patch", patch) 305 | 306 | self.synth.blocks.clear() # remove any global LFOs 307 | 308 | raw_lfo1 = synthio.LFO(rate = 0.3) #, scale=0.5, offset=0.5) # FIXME: set lfo rate by patch param 309 | lfo1 = synthio.Math( synthio.MathOperation.SCALE_OFFSET, raw_lfo1, 0.5, 0.5) # unipolar 310 | self.wave_lfo = lfo1 311 | self.synth.blocks.append(lfo1) # global lfo for wave_lfo 312 | 313 | # standard two-osc oscillator patch 314 | if patch.wave_type == WaveType.OSC: 315 | self.waveform = Waves.make_waveform('silence') # our working buffer, overwritten w/ wavemix 316 | self.waveformA = Waves.make_waveform( patch.wave ) 317 | self.waveformB = None 318 | if patch.waveB: 319 | self.waveformB = Waves.make_waveform( patch.waveB ) 320 | else: 321 | self.waveform = self.waveformA 322 | 323 | # wavetable patch 324 | elif patch.wave_type == WaveType.WTB: 325 | self.wavetable = Wavetable(patch.wave_dir+"/"+patch.wave+".WAV") 326 | self.waveform = self.wavetable.waveform 327 | 328 | self.filt_env_wave = Waves.lfo_triangle() 329 | 330 | def reload_patch(self): 331 | self.note_off_all() 332 | self.synth.blocks.clear() # clear out global wavetable LFOs (if any) 333 | self.load_patch(self.patch) 334 | 335 | def update(self): 336 | for (osc1,osc2,filt_env,amp_env) in self.voices.values(): 337 | 338 | # let Wavetable do the work # FIXME: don't need to do this per osc1 yeah? 339 | if self.patch.wave_type == WaveType.WTB: 340 | self.wave_lfo.a.rate = self.patch.wave_mix_lfo_rate # FIXME: danger 341 | wave_pos = self.wave_lfo.value * self.patch.wave_mix_lfo_amount * 10 342 | wave_pos += self.patch.wave_mix * self.wavetable.num_waves 343 | self.wavetable.set_wave_pos( wave_pos ) 344 | 345 | # else simple osc wave mixing 346 | else: 347 | if self.waveformB: 348 | #wave_mix = self.patch.wave_mix + self.wave_lfo.a.rate * self.patch.wave_mix_lfo_amount * 2 # FIXME: does not work yet 349 | wave_mix = self.patch.wave_mix 350 | osc1.waveform[:] = lerp(self.waveformA, self.waveformB, wave_mix) #self.patch.wave_mix) 351 | if self.patch.detune: 352 | osc2.waveform[:] = lerp(self.waveformA, self.waveformB, wave_mix) #self.patch.wave_mix) 353 | 354 | filt_q = self.patch.filt_q 355 | filt_mod = 0 356 | filt_f = 0 357 | filt = None 358 | 359 | # prevent filter instability around note frequency 360 | # must do this for each voice 361 | #if self.patch.filt_f / osc1.frequency < 1.2: filt_q = filt_q / 2 362 | #filt_f = max(self.patch.filt_f * filt_env.value, osc1.frequency*0.75) # filter unstable 0: 367 | filt_mod = max(0, 0.5 * 8000 * (filt_env.value/2)) # 8k/2 = max freq, 0.5 = filtermod amt 368 | filt_f = self.patch.filt_f + filt_mod 369 | filt = self.synth.low_pass_filter( filt_f,filt_q ) 370 | 371 | elif self.patch.filt_type == FiltType.HP: 372 | filt_mod = max(0, 0.5 * 8000 * (filt_env.value/2)) # 8k/2 = max freq, 0.5 = filtermod amt 373 | filt_f = self.patch.filt_f + filt_mod 374 | filt = self.synth.high_pass_filter( filt_f,filt_q ) 375 | 376 | elif self.patch.filt_type == FiltType.BP: 377 | filt_mod = max(0, 0.5 * 8000 * (filt_env.value/2)) # 8k/2 = max freq, 0.5 = filtermod amt 378 | filt_f = self.patch.filt_f + filt_mod 379 | filt = self.synth.band_pass_filter( filt_f,filt_q ) 380 | else: 381 | print("unknown filt_type:", self.patch.filt_type) 382 | 383 | #print("%s: %.1f %.1f %.1f %.1f"%(self.patch.filt_type,osc1.frequency,filt_f,self.patch.filt_f,filt_q)) 384 | osc1.filter = filt 385 | if self.patch.detune: 386 | osc2.filter = filt 387 | 388 | def note_on(self, midi_note, midi_vel=127): 389 | amp_env = self.patch.amp_env_params.make_env() 390 | 391 | #filt_env = self.patch.filt_env_params.make_env() # synthio.Envelope.value does not exist 392 | # fake an envelope with an LFO in 'once' mode 393 | filt_env = synthio.LFO(once=True, scale=0.9, offset=1.01, 394 | waveform=self.filt_env_wave, 395 | rate=self.patch.filt_env_params.attack_time, ) # always positve 396 | 397 | f = synthio.midi_to_hz(midi_note) 398 | osc1 = synthio.Note( frequency=f, waveform=self.waveform, envelope=amp_env ) 399 | osc2 = synthio.Note( frequency=f * self.patch.detune, waveform=self.waveform, envelope=amp_env ) 400 | 401 | self.voices[midi_note] = (osc1, osc2, filt_env, amp_env) 402 | self.synth.press( (osc1,osc2) ) 403 | self.synth.blocks.append(filt_env) # not tracked automaticallly by synthio 404 | 405 | def note_off(self, midi_note, midi_vel=0): 406 | (osc1,osc2,filt_env,amp_env) = self.voices.get(midi_note, (None,None,None,None)) # FIXME 407 | #print("note_off:",osc1) 408 | if osc1: # why this check? in case user tries to note_off a non-existant note 409 | self.synth.release( (osc1,osc2) ) 410 | self.voices.pop(midi_note) # FIXME: let filter run on release, check amp_env? 411 | self.synth.blocks.remove(filt_env) # FIXME: figure out how to release after note is done 412 | #print("note_off: blocks:", self.synth.blocks) 413 | 414 | def note_off_all(self): 415 | for n in self.voices.keys(): 416 | print("note_off_all:",n) 417 | self.note_off(n) 418 | 419 | def redetune(self): 420 | for (osc1,osc2,filt_env,amp_env) in self.voices.values(): 421 | osc2.frequency = osc1.frequency * self.patch.detune 422 | -------------------------------------------------------------------------------- /circuitpython/lib/qtpy_synth/winterbloom_smolmidi.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2019 Alethea Flowers for Winterbloom 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | __version__ = "0.0.0-auto.0" 24 | __repo__ = "https://github.com/theacodes/Winterbloom_SmolMIDI.git" 25 | 26 | """A minimalist MIDI library.""" 27 | 28 | # Message type constants. 29 | NOTE_OFF = 0x80 30 | NOTE_ON = 0x90 31 | AFTERTOUCH = 0xA0 32 | CONTROLLER_CHANGE = CC = 0xB0 33 | PROGRAM_CHANGE = 0xC0 34 | CHANNEL_PRESSURE = 0xD0 35 | PITCH_BEND = 0xE0 36 | SYSTEM_EXCLUSIVE = SYSEX = 0xF0 37 | SONG_POSITION = 0xF2 38 | SONG_SELECT = 0xF3 39 | BUS_SELECT = 0xF5 40 | TUNE_REQUEST = 0xF6 41 | SYSEX_END = 0xF7 42 | CLOCK = 0xF8 43 | TICK = 0xF9 44 | START = 0xFA 45 | CONTINUE = 0xFB 46 | STOP = 0xFC 47 | ACTIVE_SENSING = 0xFE 48 | SYSTEM_RESET = 0xFF 49 | 50 | _LEN_0_MESSAGES = set( 51 | [ 52 | TUNE_REQUEST, 53 | SYSEX, 54 | SYSEX_END, 55 | CLOCK, 56 | TICK, 57 | START, 58 | CONTINUE, 59 | STOP, 60 | ACTIVE_SENSING, 61 | SYSTEM_RESET, 62 | ] 63 | ) 64 | _LEN_1_MESSAGES = set([PROGRAM_CHANGE, CHANNEL_PRESSURE, SONG_SELECT, BUS_SELECT]) 65 | _LEN_2_MESSAGES = set([NOTE_OFF, NOTE_ON, AFTERTOUCH, CC, PITCH_BEND, SONG_POSITION]) 66 | 67 | 68 | def _is_channel_message(status_byte): 69 | return status_byte >= NOTE_OFF and status_byte <= PITCH_BEND + 0x0F 70 | 71 | 72 | def _read_n_bytes(port, buf, dest, num_bytes): 73 | while num_bytes: 74 | if port.readinto(buf): 75 | dest.append(buf[0]) 76 | num_bytes -= 1 77 | 78 | class Message: 79 | def __init__(self): 80 | self.type = None 81 | self.channel = None 82 | self.data = None 83 | 84 | def __bytes__(self): 85 | status_byte = self.type 86 | if self.channel: 87 | status_byte |= self.channel 88 | 89 | return bytes([status_byte] + list(self.data if self.data else [])) 90 | 91 | 92 | class MidiIn: 93 | def __init__(self, port, enable_running_status=False): 94 | self._port = port 95 | self._read_buf = bytearray(1) 96 | self._running_status_enabled = enable_running_status 97 | self._running_status = None 98 | self._outstanding_sysex = False 99 | self._error_count = 0 100 | 101 | @property 102 | def error_count(self): 103 | return self._error_count 104 | 105 | def receive(self): 106 | # Before we do anything, check and see if there's an unprocessed 107 | # sysex message pending. If so, throw it away. The caller has 108 | # to call receive_sysex if they care about the bytes. 109 | if self._outstanding_sysex: 110 | self.receive_sysex(0) 111 | 112 | # Read the status byte for the next message. 113 | result = self._port.readinto(self._read_buf) 114 | 115 | # No message ready. 116 | if not result: 117 | return None 118 | 119 | message = Message() 120 | data_bytes = bytearray() 121 | 122 | # Is this a status byte? 123 | status_byte = self._read_buf[0] 124 | is_status = status_byte & 0x80 125 | 126 | # If not, see if we have a running status byte. 127 | if not is_status: 128 | if self._running_status_enabled and self._running_status: 129 | data_bytes = [status_byte] 130 | status_byte = self._running_status 131 | # If not a status byte and no running status, this is 132 | # invalid data. 133 | else: 134 | self._error_count += 1 135 | return None 136 | 137 | # Is this a channel message, if so, let's figure out the right 138 | # message type and set the message's channel property. 139 | if _is_channel_message(status_byte): 140 | # Only set the running status byte for channel messages. 141 | self._running_status = status_byte 142 | # Mask off the channel nibble. 143 | message.type = status_byte & 0xF0 144 | message.channel = status_byte & 0x0F 145 | else: 146 | message.type = status_byte 147 | 148 | # Read the appropriate number of bytes for each message type. 149 | if message.type in _LEN_2_MESSAGES: 150 | _read_n_bytes(self._port, self._read_buf, data_bytes, 2 - len(data_bytes)) 151 | message.data = data_bytes 152 | elif message.type in _LEN_1_MESSAGES: 153 | _read_n_bytes(self._port, self._read_buf, data_bytes, 1 - len(data_bytes)) 154 | message.data = data_bytes 155 | 156 | # If this is a sysex message, set the pending sysex flag so we 157 | # can throw the message away if the user doesn't process it. 158 | if message.type == SYSEX: 159 | self._outstanding_sysex = True 160 | 161 | # Check the data bytes for corruption. If the data bytes have any status bytes 162 | # embedded, it probably means the buffer overflowed. Either way, discard the 163 | # message. 164 | # TODO: Figure out a better way to detect and deal with this upstream. 165 | for b in data_bytes: 166 | if b & 0x80: 167 | self._error_count += 1 168 | return None 169 | 170 | return message 171 | 172 | def receive_sysex(self, max_length): 173 | """Receives the next outstanding sysex message. 174 | 175 | Returns a tuple: the first item is the bytearray of the 176 | sysex message. The second is a boolean that indicates 177 | if the message was truncated or not. 178 | 179 | This must only be called after getting a sysex message from 180 | receive and must be called before invoking receive again. 181 | """ 182 | self._outstanding_sysex = False 183 | 184 | out = bytearray() 185 | length = 0 186 | buf = bytearray(1) 187 | truncated = False 188 | 189 | # This reads one byte at a time so we don't read past the 190 | # end byte. There may be more efficient ways to do this 191 | # but sysex messages should be relatively rare in practice, 192 | # so I'm not sure how much benefit we'll get. 193 | while length < max_length: 194 | self._port.readinto(buf) 195 | if buf[0] == SYSEX_END: 196 | break 197 | out.extend(buf) 198 | length += 1 199 | 200 | # We exceeded the length. 201 | else: 202 | truncated = True 203 | # Ignore the rest of the message by reading and throwing away 204 | # bytes until we get to SYSEX_END. 205 | while buf[0] != SYSEX_END: 206 | self._port.readinto(buf) 207 | 208 | return out, truncated 209 | -------------------------------------------------------------------------------- /circuitpython/requirements.txt: -------------------------------------------------------------------------------- 1 | asyncio 2 | neopixel 3 | adafruit_wave 4 | adafruit_debouncer 5 | adafruit_displayio_ssd1306 6 | adafruit_display_text 7 | -------------------------------------------------------------------------------- /circuitpython/simpletouchsynth/code.py: -------------------------------------------------------------------------------- 1 | # simpletouchsynth_code.py -- simple touchpad synth, touch pads more to move filter freq 2 | # 12 Jul 2023 - @todbot / Tod Kurt 3 | # part of https://github.com/todbot/qtpy_synth 4 | # 5 | # Needed libraries to install: 6 | # circup install asyncio neopixel adafruit_debouncer adafruit_displayio_ssd1306 adafruit_display_text 7 | # Also install "qtpy_synth" directory in CIRCUITPY 8 | # 9 | 10 | import asyncio 11 | import time 12 | import random 13 | import synthio 14 | import ulab.numpy as np 15 | import usb_midi 16 | 17 | import displayio, terminalio, vectorio 18 | from adafruit_display_text import bitmap_label as label 19 | 20 | from qtpy_synth.hardware import Hardware 21 | import qtpy_synth.winterbloom_smolmidi as smolmidi 22 | 23 | class SynthConfig(): 24 | def __init__(self): 25 | self.filter_type = 'lpf' 26 | self.filter_f = 2000 27 | self.filter_q = 1.2 28 | self.filter_mod = 0 29 | 30 | qts = Hardware() 31 | cfg = SynthConfig() 32 | 33 | touch_midi_notes = [40, 48, 52, 60] # can be float 34 | notes_playing = {} # dict of notes currently playing 35 | 36 | # let's get the midi going 37 | midi_usb_in = smolmidi.MidiIn(usb_midi.ports[0]) 38 | midi_uart_in = smolmidi.MidiIn(qts.midi_uart) 39 | 40 | # set up some default synth parameters 41 | wave_saw = np.linspace(30000,-30000, num=512, dtype=np.int16) 42 | # default squ is too clippy, should be 3dB down or so 43 | 44 | amp_env = synthio.Envelope(sustain_level=0.8, release_time=0.6, attack_time=0.001) 45 | qts.synth.envelope = amp_env 46 | 47 | # set up display with our 3 chunks of info 48 | disp_group = displayio.Group() 49 | qts.display.root_group = disp_group 50 | 51 | labels_pos = ( (5,5), (50,5), (100,5), (15,50) ) # filter_f, filter_type, filter_q, hellotext 52 | disp_info = displayio.Group() 53 | for (x,y) in labels_pos: 54 | disp_info.append( label.Label(terminalio.FONT, text="-", x=x, y=y) ) 55 | disp_group.append(disp_info) 56 | disp_info[3].text = "simpletouchsynth" 57 | 58 | def map_range(s, a1, a2, b1, b2): return b1 + ((s - a1) * (b2 - b1) / (a2 - a1)) 59 | 60 | def display_update(): 61 | f_str = "%4d" % (cfg.filter_f + cfg.filter_mod) 62 | q_str = "%1.1f" % cfg.filter_q 63 | 64 | if f_str != disp_info[0].text: 65 | disp_info[0].text = f_str 66 | 67 | if cfg.filter_type != disp_info[1].text: 68 | disp_info[1].text = cfg.filter_type 69 | 70 | if q_str != disp_info[2].text: 71 | disp_info[2].text = q_str 72 | print("edit q", q_str) 73 | 74 | def note_on( notenum, vel=64): 75 | print("note_on", notenum, vel) 76 | cfg.filter_mod = (vel/127) * 1500 77 | f = synthio.midi_to_hz(notenum) 78 | note = synthio.Note( frequency=f, waveform=wave_saw, filter=make_filter() ) 79 | notes_playing[notenum] = note 80 | qts.synth.press( note ) 81 | qts.led.fill(0xff00ff) 82 | 83 | def note_off( notenum, vel=0): 84 | print("note_off", notenum, vel) 85 | if note := notes_playing[notenum]: 86 | qts.synth.release( note ) 87 | qts.led.fill(0) 88 | 89 | # how to do this 90 | def touch_hold(i,v): # callback 91 | vn = min(max(0, v), 2000) # ensure touch info stays positive 92 | cfg.filter_mod = (vn/2000) * 3000 # range 0-3000 93 | print("hold %d %d %d" % (i,vn, cfg.filter_mod)) 94 | 95 | filter_types = ['lpf', 'hpf', 'bpf'] 96 | 97 | def make_filter(): 98 | freq = cfg.filter_f + cfg.filter_mod 99 | if cfg.filter_type == 'lpf': 100 | filter = qts.synth.low_pass_filter(freq, cfg.filter_q) 101 | elif cfg.filter_type == 'hpf': 102 | filter = qts.synth.high_pass_filter(freq, cfg.filter_q) 103 | elif cfg.filter_type == 'bpf': 104 | filter = qts.synth.band_pass_filter(freq, cfg.filter_q) 105 | else: 106 | print("unknown filter type", cfg.filter_type) 107 | return filter 108 | 109 | 110 | # -------------------------------------------------------- 111 | 112 | async def display_updater(): 113 | while True: 114 | print("knobs:", int(qts.knobA//255), int(qts.knobB//255), 115 | #qts.touchins[0].raw_value, qts.touchins[1].raw_value, 116 | #qts.touchins[3].raw_value, qts.touchins[2].raw_value 117 | ) 118 | display_update() 119 | await asyncio.sleep(0.1) 120 | 121 | async def input_handler(): 122 | while True: 123 | (knobA, knobB) = qts.read_pots() 124 | 125 | if key := qts.check_key(): 126 | if key.pressed: 127 | ftpos = (filter_types.index(cfg.filter_type)+1) % len(filter_types) 128 | cfg.filter_type = filter_types[ ftpos ] 129 | 130 | if touches := qts.check_touch(): 131 | for touch in touches: 132 | if touch.pressed: note_on( touch_midi_notes[touch.key_number] ) 133 | if touch.released: note_off( touch_midi_notes[touch.key_number] ) 134 | 135 | qts.check_touch_hold(touch_hold) 136 | 137 | cfg.filter_f = map_range( knobA, 0,65535, 30, 8000) 138 | cfg.filter_q = map_range( knobB, 0,65535, 0.1, 3.0) 139 | 140 | await asyncio.sleep(0.01) 141 | 142 | async def synth_updater(): 143 | # for any notes playing, adjust its filter in realtime 144 | while True: 145 | for n in notes_playing.values(): 146 | if n: 147 | n.filter = make_filter() 148 | await asyncio.sleep(0.01) 149 | 150 | async def midi_handler(): 151 | while True: 152 | while msg := midi_usb_in.receive() or midi_uart_in.receive(): 153 | if msg.type == smolmidi.NOTE_ON: 154 | note_on( msg.data[0], msg.data[1] ) 155 | elif msg.type == smolmidi.NOTE_OFF: 156 | note_off( msg.data[0], msg.data[1] ) 157 | await asyncio.sleep(0.001) 158 | 159 | print("-- qtpy_synth simpletouchsynth ready --") 160 | 161 | async def main(): 162 | task1 = asyncio.create_task(display_updater()) 163 | task2 = asyncio.create_task(input_handler()) 164 | task3 = asyncio.create_task(synth_updater()) 165 | task4 = asyncio.create_task(midi_handler()) 166 | await asyncio.gather(task1,task2,task3,task4) 167 | 168 | asyncio.run(main()) 169 | -------------------------------------------------------------------------------- /circuitpython/wavesynth/code.py: -------------------------------------------------------------------------------- 1 | # wavesynth_code.py -- wavesynth for qtpy_synth 2 | # 28 Jul 2023 - @todbot / Tod Kurt 3 | # part of https://github.com/todbot/qtpy_synth 4 | # 5 | # UI: 6 | # - Display shows four lines of two parameters each 7 | # - The current editable parameter pair is underlined, adjustable by the knobs 8 | # - The knobs have "catchup" logic ( 9 | # (knob must pass through the displayed value before value can be changed) 10 | # 11 | # - Key tap (press & release) == change what editable line (what knobs are editing) 12 | # - Key hold + touch press = load patch 1,2,3,4 (turned off currently) 13 | # - Touch press/release == play note / release note 14 | # 15 | 16 | import asyncio 17 | import time 18 | import usb_midi 19 | 20 | from qtpy_synth.hardware import Hardware 21 | from qtpy_synth.synthio_instrument import WavePolyTwoOsc, Patch, FiltType 22 | import qtpy_synth.winterbloom_smolmidi as smolmidi 23 | 24 | from wavesynth_display import WavesynthDisplay 25 | 26 | import microcontroller 27 | microcontroller.cpu.frequency = 250_000_000 28 | 29 | time.sleep(2) # let USB settle down 30 | 31 | touch_midi_notes = [40, 48, 52, 55] # can be float 32 | 33 | patch1 = Patch('oneuno') 34 | patch2 = Patch('twotoo') 35 | patch3 = Patch('three') 36 | patch4 = Patch('fourfor') 37 | patches = (patch1, patch2, patch3, patch4) 38 | 39 | patch1.filt_env_params.attack_time = 0.1 40 | patch1.amp_env_params.attack_time = 0.01 41 | 42 | patch2.filt_type = FiltType.HP 43 | patch2.wave = 'square' 44 | patch2.detune=1.01 45 | patch2.filt_env_params.attack_time = 0.0 # turn off filter FIXME 46 | patch2.amp_env_params.release_time = 1.0 47 | 48 | patch3.waveformB = 'square' # show off wavemixing 49 | patch3.filt_type = FiltType.BP 50 | 51 | patch4.wave_type = 'wtb' 52 | patch4.wave = 'PLAITS02' # 'MICROW02' 'BRAIDS04' 53 | patch4.wave_mix_lfo_amount = 0.23 54 | #patch4.detune = 0 # disable 2nd oscillator 55 | patch4.amp_env_params.release_time = 0.5 56 | 57 | print("--- qtpy_synth wavesynth starting up ---") 58 | 59 | qts = Hardware() 60 | inst = WavePolyTwoOsc(qts.synth, patch4) 61 | wavedisp = WavesynthDisplay(qts.display, inst.patch) 62 | 63 | # let's get the midi going 64 | midi_usb_in = smolmidi.MidiIn(usb_midi.ports[0]) 65 | midi_uart_in = smolmidi.MidiIn(qts.midi_uart) 66 | 67 | def map_range(s, a1, a2, b1, b2): return b1 + ((s - a1) * (b2 - b1) / (a2 - a1)) 68 | 69 | 70 | async def instrument_updater(): 71 | while True: 72 | inst.update() 73 | await asyncio.sleep(0.01) # as fast as possible 74 | 75 | async def display_updater(): 76 | while True: 77 | wavedisp.display_update() 78 | await asyncio.sleep(0.1) 79 | 80 | 81 | async def midi_handler(): 82 | while True: 83 | while msg := midi_usb_in.receive() or midi_uart_in.receive(): 84 | if msg.type == smolmidi.NOTE_ON: 85 | inst.note_on(msg.data[0]) 86 | qts.led.fill(0xff00ff) 87 | elif msg.type == smolmidi.NOTE_OFF: 88 | inst.note_off(msg.data[0]) 89 | qts.led.fill(0x000000) 90 | elif msg.type == smolmidi.CC: 91 | ccnum = msg.data[0] 92 | ccval = msg.data[1] 93 | qts.led.fill(ccval) 94 | if ccnum == 71: # "sound controller 1" 95 | new_wave_mix = ccval/127 96 | print("wave_mix:", new_wave_mix) 97 | inst.patch.wave_mix = new_wave_mix 98 | elif ccnum == 1: # mod wheel 99 | inst.patch.wave_mix_lfo_amount = ccval/127 * 50 100 | #inst.patch.wave_mix_lfo_rate = msg.value/127 * 5 101 | elif ccnum == 74: # filter cutoff 102 | inst.patch.filt_f = ccval/127 * 8000 103 | 104 | await asyncio.sleep(0.001) 105 | 106 | 107 | async def input_handler(): 108 | 109 | # fixme: put these in qtpy_synth.py? no I think they are part of this "app" 110 | knob_mode = 0 # 0=frequency, 1=wavemix, 2=, 3= 111 | key_held = False 112 | key_with_touch = False 113 | knob_saves = [ (0,0) for _ in range(4) ] # list of knob state pairs 114 | param_saves = [ (0,0) for _ in range(4) ] # list of param state pairs for knobs 115 | knobA_pickup, knobB_pickup = False, False 116 | knobA, knobB = 0,0 117 | 118 | def reload_patch(wave_select): 119 | print("reload patch!", wave_select) 120 | # the below seems like the wrong way to do this, needlessly complex 121 | inst.patch.set_by_wave_select( wave_select ) 122 | inst.reload_patch() 123 | param_saves[0] = wavedisp.wave_select_pos(), inst.patch.wave_mix 124 | param_saves[1] = inst.patch.detune, inst.patch.wave_mix_lfo_amount 125 | param_saves[2] = inst.patch.filt_type, inst.patch.filt_f 126 | param_saves[3] = inst.patch.filt_q, inst.patch.filt_env_params.attack_time 127 | 128 | while True: 129 | # KNOB input 130 | (knobA_new, knobB_new) = qts.read_pots() 131 | 132 | # simple knob pickup logic: if the real knob is close enough to 133 | if abs(knobA - knobA_new) <= 1000: # knobs range 0-65535 134 | knobA_pickup = True 135 | if abs(knobB - knobB_new) <= 1000: 136 | knobB_pickup = True 137 | 138 | if knobA_pickup: 139 | knobA = knobA_new 140 | if knobB_pickup: 141 | knobB = knobB_new 142 | 143 | # TOUCH input 144 | if touches := qts.check_touch(): 145 | for touch in touches: 146 | 147 | if touch.pressed: 148 | if key_held: # load a patch 149 | print("load patch", touch.key_number) 150 | # disable this for now 151 | #inst.load_patch(patches[i]) 152 | #qts.patch = patches[i] 153 | wavedisp.display_update() 154 | key_with_touch = True 155 | else: # trigger a note 156 | qts.led.fill(0xff00ff) 157 | midi_note = touch_midi_notes[touch.key_number] 158 | inst.note_on(midi_note) 159 | 160 | if touch.released: 161 | if key_with_touch: 162 | key_with_touch = False 163 | else: 164 | qts.led.fill(0) 165 | midi_note = touch_midi_notes[touch.key_number] 166 | inst.note_off(midi_note) 167 | 168 | # KEY input 169 | if key := qts.check_key(): 170 | if key.pressed: 171 | key_held = True 172 | if key.released: 173 | key_held = False 174 | if not key_with_touch: # key tap == change what knobs do 175 | # turn off pickup mode since we change what knobs do 176 | knobA_pickup, knobB_pickup = False, False 177 | knob_saves[knob_mode] = knobA, knobB # save knob positions 178 | knob_mode = (knob_mode + 1) % 4 # FIXME: make a max_knob_mode 179 | knobA, knobB = knob_saves[knob_mode] # retrive saved knob positions 180 | print("knob mode:",knob_mode, knobA, knobB) 181 | wavedisp.selected_info = knob_mode # FIXME 182 | 183 | # Handle parameter changes depending on knob mode 184 | if knob_mode == 0: # wave selection & wave_mix 185 | 186 | wave_select_pos, wave_mix = param_saves[knob_mode] 187 | 188 | if knobA_pickup: 189 | wave_select_pos = map_range( knobA, 0,65535, 0, len(wavedisp.wave_selects)-1) 190 | if knobB_pickup: 191 | wave_mix = map_range( knobB, 0,65535, 0, 1) 192 | 193 | param_saves[knob_mode] = wave_select_pos, wave_mix 194 | 195 | wave_select = wavedisp.wave_selects[ int(wave_select_pos) ] 196 | 197 | if inst.patch.wave_select() != wave_select: 198 | reload_patch(wave_select) 199 | 200 | inst.patch.wave_mix = wave_mix 201 | 202 | elif knob_mode == 1: # osc detune & wave_mix lfo 203 | 204 | detune, wave_lfo = param_saves[knob_mode] 205 | 206 | if knobA_pickup: 207 | detune = map_range(knobA, 300,65300, 1, 1.1) # RP2040 has bad ADC 208 | if knobB_pickup: 209 | wave_lfo = map_range(knobB, 0,65535, 0, 1) 210 | 211 | param_saves[knob_mode] = detune, wave_lfo 212 | 213 | inst.patch.wave_mix_lfo_amount = wave_lfo 214 | inst.patch.detune = detune 215 | 216 | 217 | elif knob_mode == 2: # filter type and filter freq 218 | filt_type, filt_f = param_saves[knob_mode] 219 | 220 | if knobA_pickup: 221 | filt_type = int(map_range(knobA, 0,65535, 0,3)) 222 | if knobB_pickup: 223 | filt_f = map_range(knobB, 300,65300, 100, 8000) 224 | 225 | param_saves[knob_mode] = filt_type, filt_f 226 | 227 | inst.patch.filt_type = filt_type 228 | inst.patch.filt_f = filt_f 229 | 230 | elif knob_mode == 3: 231 | filt_q, filt_env = param_saves[knob_mode] 232 | 233 | if knobA_pickup: 234 | filt_q = map_range(knobA, 0,65535, 0.5,2.5) 235 | if knobB_pickup: 236 | filt_env = map_range(knobB, 300,65300, 1, 0.01) 237 | 238 | param_saves[knob_mode] = filt_q, filt_env 239 | 240 | inst.patch.filt_q = filt_q 241 | inst.patch.filt_env_params.attack_time = filt_env 242 | 243 | else: 244 | pass 245 | 246 | await asyncio.sleep(0.02) 247 | 248 | 249 | print("--- qtpy_synth wavesynth ready ---") 250 | 251 | async def main(): 252 | task1 = asyncio.create_task(display_updater()) 253 | task2 = asyncio.create_task(input_handler()) 254 | task3 = asyncio.create_task(midi_handler()) 255 | task4 = asyncio.create_task(instrument_updater()) 256 | await asyncio.gather(task1, task2, task3, task4) 257 | 258 | asyncio.run(main()) 259 | -------------------------------------------------------------------------------- /circuitpython/wavesynth/wav/303.WAV: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/qtpy_synth/e9ad4358de26e5973b7daa7514ee27f6ef0a1e8e/circuitpython/wavesynth/wav/303.WAV -------------------------------------------------------------------------------- /circuitpython/wavesynth/wav/ADDITIVE.WAV: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/qtpy_synth/e9ad4358de26e5973b7daa7514ee27f6ef0a1e8e/circuitpython/wavesynth/wav/ADDITIVE.WAV -------------------------------------------------------------------------------- /circuitpython/wavesynth/wav/AKVF_GRA.WAV: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/qtpy_synth/e9ad4358de26e5973b7daa7514ee27f6ef0a1e8e/circuitpython/wavesynth/wav/AKVF_GRA.WAV -------------------------------------------------------------------------------- /circuitpython/wavesynth/wav/ANALOG_W.WAV: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/qtpy_synth/e9ad4358de26e5973b7daa7514ee27f6ef0a1e8e/circuitpython/wavesynth/wav/ANALOG_W.WAV -------------------------------------------------------------------------------- /circuitpython/wavesynth/wav/BRAIDS01.WAV: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/qtpy_synth/e9ad4358de26e5973b7daa7514ee27f6ef0a1e8e/circuitpython/wavesynth/wav/BRAIDS01.WAV -------------------------------------------------------------------------------- /circuitpython/wavesynth/wav/BRAIDS02.WAV: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/qtpy_synth/e9ad4358de26e5973b7daa7514ee27f6ef0a1e8e/circuitpython/wavesynth/wav/BRAIDS02.WAV -------------------------------------------------------------------------------- /circuitpython/wavesynth/wav/BRAIDS03.WAV: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/qtpy_synth/e9ad4358de26e5973b7daa7514ee27f6ef0a1e8e/circuitpython/wavesynth/wav/BRAIDS03.WAV -------------------------------------------------------------------------------- /circuitpython/wavesynth/wav/BRAIDS04.WAV: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/qtpy_synth/e9ad4358de26e5973b7daa7514ee27f6ef0a1e8e/circuitpython/wavesynth/wav/BRAIDS04.WAV -------------------------------------------------------------------------------- /circuitpython/wavesynth/wav/MICROW02.WAV: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/qtpy_synth/e9ad4358de26e5973b7daa7514ee27f6ef0a1e8e/circuitpython/wavesynth/wav/MICROW02.WAV -------------------------------------------------------------------------------- /circuitpython/wavesynth/wav/PLAITS01.WAV: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/qtpy_synth/e9ad4358de26e5973b7daa7514ee27f6ef0a1e8e/circuitpython/wavesynth/wav/PLAITS01.WAV -------------------------------------------------------------------------------- /circuitpython/wavesynth/wav/PLAITS02.WAV: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/qtpy_synth/e9ad4358de26e5973b7daa7514ee27f6ef0a1e8e/circuitpython/wavesynth/wav/PLAITS02.WAV -------------------------------------------------------------------------------- /circuitpython/wavesynth/wav/PLAITS03.WAV: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/qtpy_synth/e9ad4358de26e5973b7daa7514ee27f6ef0a1e8e/circuitpython/wavesynth/wav/PLAITS03.WAV -------------------------------------------------------------------------------- /circuitpython/wavesynth/wavesynth_display.py: -------------------------------------------------------------------------------- 1 | # wavesynth_display.py -- wavesynth display management for qtpy_synth 2 | # 28 Jul 2023 - @todbot / Tod Kurt 3 | # part of https://github.com/todbot/qtpy_synth 4 | 5 | import os 6 | import displayio, terminalio, vectorio 7 | from adafruit_display_text import bitmap_label as label 8 | 9 | from qtpy_synth.synthio_instrument import FiltType 10 | 11 | # class WavesynthDisplay(displayio.Group): 12 | # def __init__(self, display): 13 | # super().__init(x=0,y=0,scale=1) 14 | # self.display = display 15 | # self.display_setup() 16 | 17 | class WavesynthDisplay: 18 | def __init__(self, display, patch): 19 | self.display = display 20 | self.patch = patch 21 | self.selected_info = 0 # which part of the display is currently selected 22 | self.update_wave_selects() 23 | self.display_setup() 24 | self.display_update() 25 | print("WavesynthDisplay:init: patch=",patch) 26 | 27 | def display_setup(self): 28 | disp_group = displayio.Group() 29 | self.display.root_group = disp_group 30 | 31 | lwave_sel = label.Label(terminalio.FONT, text=self.patch.wave_select(), x=2, y=6) 32 | lwave_mix = label.Label(terminalio.FONT, text=str(self.patch.wave_mix), x=80, y=6) 33 | 34 | ldetune = label.Label(terminalio.FONT, text="-", x=2, y=19) 35 | lwave_lfo= label.Label(terminalio.FONT, text="-", x=75, y=19) 36 | 37 | lfilt_type = label.Label(terminalio.FONT, text="-", x=2,y=32) 38 | lfilt_f = label.Label(terminalio.FONT, text="-", x=75,y=32) 39 | 40 | lfilt_q = label.Label(terminalio.FONT, text="-", x=2,y=45) 41 | lfilt_env= label.Label(terminalio.FONT, text="-", x=70,y=45) 42 | 43 | self.disp_line1 = displayio.Group() 44 | for l in (lwave_sel, lwave_mix): 45 | self.disp_line1.append(l) 46 | disp_group.append(self.disp_line1) 47 | 48 | self.disp_line2 = displayio.Group() 49 | for l in (ldetune, lwave_lfo): 50 | self.disp_line2.append(l) 51 | disp_group.append(self.disp_line2) 52 | 53 | self.disp_line3 = displayio.Group() 54 | for l in (lfilt_type, lfilt_f): 55 | self.disp_line3.append(l) 56 | disp_group.append(self.disp_line3) 57 | 58 | self.disp_line4 = displayio.Group() 59 | for l in (lfilt_q, lfilt_env): 60 | self.disp_line4.append(l) 61 | disp_group.append(self.disp_line4) 62 | 63 | # selection lines 64 | pal = displayio.Palette(1) 65 | pal[0] = 0xffffff 66 | selectF = vectorio.Rectangle(pixel_shader=pal, width=128, height=1, x=0, y=6 + 8) 67 | selectW = vectorio.Rectangle(pixel_shader=pal, width=128, height=1, x=0, y=19 + 8) 68 | selectG = vectorio.Rectangle(pixel_shader=pal, width=128, height=1, x=0, y=32 + 8) 69 | selectH = vectorio.Rectangle(pixel_shader=pal, width=128, height=1, x=0, y=45 + 8) 70 | 71 | self.disp_selects = displayio.Group() 72 | for s in (selectF,selectW,selectG, selectH): 73 | s.hidden=True 74 | self.disp_selects.append(s) 75 | disp_group.append(self.disp_selects) 76 | 77 | def display_update(self): 78 | self.disp_select() 79 | self.display_update_line1() 80 | self.display_update_line2() 81 | self.display_update_line3() 82 | self.display_update_line4() 83 | 84 | def disp_select(self): 85 | for s in self.disp_selects: 86 | s.hidden = True 87 | self.disp_selects[self.selected_info].hidden = False 88 | 89 | def display_update_line1(self): 90 | wave_select = self.patch.wave_select() 91 | wave_mix = "%.2f:mix" % self.patch.wave_mix 92 | 93 | if self.disp_line1[0].text != wave_select: 94 | self.disp_line1[0].text = wave_select 95 | if self.disp_line1[1].text != wave_mix: 96 | self.disp_line1[1].text = wave_mix 97 | 98 | def display_update_line2(self): 99 | detune = "detun:%1.3f" % self.patch.detune 100 | wave_lfo = "%1.2f:wlfo" % self.patch.wave_mix_lfo_amount 101 | 102 | if self.disp_line2[0].text != detune: 103 | self.disp_line2[0].text = detune 104 | if self.disp_line2[1].text != wave_lfo: 105 | self.disp_line2[1].text = wave_lfo 106 | 107 | def display_update_line3(self): 108 | filt_type = "filter:"+FiltType.str(self.patch.filt_type) 109 | filt_f = "freq:%1.1fk" % (self.patch.filt_f / 1000) 110 | 111 | if self.disp_line3[0].text != filt_type: 112 | self.disp_line3[0].text = filt_type 113 | if self.disp_line3[1].text != filt_f: 114 | self.disp_line3[1].text = filt_f 115 | 116 | def display_update_line4(self): 117 | filt_q = "filtq:%1.1f" % self.patch.filt_q 118 | filt_env = "fenv:%1.2f" % self.patch.filt_env_params.attack_time 119 | 120 | if self.disp_line4[0].text != filt_q: 121 | self.disp_line4[0].text = filt_q 122 | if self.disp_line4[1].text != filt_env: 123 | print("!") 124 | self.disp_line4[1].text = filt_env 125 | 126 | # utility methods for dealing with these "wave_selects" I've gotten myself into 127 | 128 | def update_wave_selects(self): # fixme: why isn't this a Patch class method? 129 | wave_selects = [ 130 | "osc:SAW/TRI", 131 | "osc:SAW/SQU", 132 | "osc:SAW/SIN", 133 | "osc:SQU/SIN" 134 | ] 135 | # fixme: check for bad/none dir_path 136 | for path in os.listdir(self.patch.wave_dir): 137 | path = path.upper() 138 | if path.endswith('.WAV') and not path.startswith('.'): 139 | wave_selects.append("wtb:"+path.replace('.WAV','')) 140 | self.wave_selects = wave_selects 141 | 142 | def wave_select_pos(self): 143 | print("wave_select_pos:",self.patch.wave_select()) 144 | return self.wave_selects.index( self.patch.wave_select() ) # fixme: this seems wrong 145 | 146 | def wave_select(self): 147 | return self.patch.wave_select() 148 | -------------------------------------------------------------------------------- /docs/qtpy_synth_case1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/qtpy_synth/e9ad4358de26e5973b7daa7514ee27f6ef0a1e8e/docs/qtpy_synth_case1.jpg -------------------------------------------------------------------------------- /docs/qtpy_synth_proto2a.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/qtpy_synth/e9ad4358de26e5973b7daa7514ee27f6ef0a1e8e/docs/qtpy_synth_proto2a.jpg -------------------------------------------------------------------------------- /enclosure/qtpy_synth_case.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/qtpy_synth/e9ad4358de26e5973b7daa7514ee27f6ef0a1e8e/enclosure/qtpy_synth_case.stl -------------------------------------------------------------------------------- /schematics/gerbers/qtpy_synth_proto2b_2023-08-05.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/qtpy_synth/e9ad4358de26e5973b7daa7514ee27f6ef0a1e8e/schematics/gerbers/qtpy_synth_proto2b_2023-08-05.zip -------------------------------------------------------------------------------- /schematics/qtpy_synth_proto2b.brd: -------------------------------------------------------------------------------- 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 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | NeoPixel BFF 207 | TX 208 | SCL 209 | 5V 210 | A1 211 | A2 212 | A0 213 | A3 214 | USB 215 | NeoPixel Driver 216 | Neo+ <-> 5V 217 | QT Py. Solder 218 | back to back 219 | Signal on A3 220 | 221 | 222 | G 223 | 3V 224 | MO 225 | MI 226 | SDA 227 | SCK 228 | RX 229 | - 230 | S 231 | + 232 | Sig 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 1 251 | 2 252 | 3 253 | 4 254 | qtpy_synth 255 | MIDI IN 256 | AUDIO OUT 257 | @todbot 258 | qtpy_synth 259 | github.com/todbot/qtpy_synth 260 | 12 Jul 2023 - @todbot 261 | Parts: 262 | - QTPy RP2040 263 | - 0.9" I2C OLED SSD1306 264 | - 2x 10k pots 265 | - tact switch 6mm 266 | - 2x PJ320D 3.5mm jacks 267 | - IC1: H11L1 optoisolator 268 | - D1: 1N4148 269 | - R1-R4: 1M, R5: 4.7k, R6,R7: 220, R8: 100: R9: 1.8k 270 | - C1,C2: 100nF, C3: 47uF 271 | AUDIO 272 | LINE 273 | OUT 274 | MIDI 275 | IN 276 | TRS-A 277 | Gnd VCC SCL SDA 278 | A0 279 | 280 | A1 281 | 282 | A2 283 | 284 | A3 285 | 286 | SDA 287 | 288 | SCL 289 | 290 | TX 291 | 5V 292 | 293 | GND 294 | 295 | 3V 296 | 297 | MO 298 | 299 | MI 300 | 301 | SCK 302 | 303 | RX 304 | 5V 305 | 306 | GND 307 | 308 | 3V 309 | 310 | AUDIO 311 | 312 | TP3 313 | 314 | TP4 315 | 316 | MIDI 317 | POTA 318 | 319 | POTB 320 | 321 | TP2 322 | 323 | TP1 324 | 325 | SDA 326 | 327 | SCL 328 | 329 | SW 330 | qtpy_synth_proto2b 331 | works with CircuitPython! 332 | 333 | 334 | 335 | 336 | 337 | <b>Resistors, Capacitors, Inductors</b><p> 338 | Based on the previous libraries: 339 | <ul> 340 | <li>r.lbr 341 | <li>cap.lbr 342 | <li>cap-fe.lbr 343 | <li>captant.lbr 344 | <li>polcap.lbr 345 | <li>ipc-smd.lbr 346 | </ul> 347 | All SMD packages are defined according to the IPC specifications and CECC<p> 348 | <author>Created by librarian@cadsoft.de</author><p> 349 | <p> 350 | for Electrolyt Capacitors see also :<p> 351 | www.bccomponents.com <p> 352 | www.panasonic.com<p> 353 | www.kemet.com<p> 354 | http://www.secc.co.jp/pdf/os_e/2004/e_os_all.pdf <b>(SANYO)</b> 355 | <p> 356 | for trimmer refence see : <u>www.electrospec-inc.com/cross_references/trimpotcrossref.asp</u><p> 357 | 358 | <table border=0 cellspacing=0 cellpadding=0 width="100%" cellpaddding=0> 359 | <tr valign="top"> 360 | 361 | <! <td width="10">&nbsp;</td> 362 | <td width="90%"> 363 | 364 | <b><font color="#0000FF" size="4">TRIM-POT CROSS REFERENCE</font></b> 365 | <P> 366 | <TABLE BORDER=0 CELLSPACING=1 CELLPADDING=2> 367 | <TR> 368 | <TD COLSPAN=8> 369 | <FONT SIZE=3 FACE=ARIAL><B>RECTANGULAR MULTI-TURN</B></FONT> 370 | </TD> 371 | </TR> 372 | <TR> 373 | <TD ALIGN=CENTER> 374 | <B> 375 | <FONT SIZE=3 FACE=ARIAL color="#FF0000">BOURNS</FONT> 376 | </B> 377 | </TD> 378 | <TD ALIGN=CENTER> 379 | <B> 380 | <FONT SIZE=3 FACE=ARIAL color="#FF0000">BI&nbsp;TECH</FONT> 381 | </B> 382 | </TD> 383 | <TD ALIGN=CENTER> 384 | <B> 385 | <FONT SIZE=3 FACE=ARIAL color="#FF0000">DALE-VISHAY</FONT> 386 | </B> 387 | </TD> 388 | <TD ALIGN=CENTER> 389 | <B> 390 | <FONT SIZE=3 FACE=ARIAL color="#FF0000">PHILIPS/MEPCO</FONT> 391 | </B> 392 | </TD> 393 | <TD ALIGN=CENTER> 394 | <B> 395 | <FONT SIZE=3 FACE=ARIAL color="#FF0000">MURATA</FONT> 396 | </B> 397 | </TD> 398 | <TD ALIGN=CENTER> 399 | <B> 400 | <FONT SIZE=3 FACE=ARIAL color="#FF0000">PANASONIC</FONT> 401 | </B> 402 | </TD> 403 | <TD ALIGN=CENTER> 404 | <B> 405 | <FONT SIZE=3 FACE=ARIAL color="#FF0000">SPECTROL</FONT> 406 | </B> 407 | </TD> 408 | <TD ALIGN=CENTER> 409 | <B> 410 | <FONT SIZE=3 FACE=ARIAL color="#FF0000">MILSPEC</FONT> 411 | </B> 412 | </TD><TD>&nbsp;</TD> 413 | </TR> 414 | <TR> 415 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3 > 416 | 3005P<BR> 417 | 3006P<BR> 418 | 3006W<BR> 419 | 3006Y<BR> 420 | 3009P<BR> 421 | 3009W<BR> 422 | 3009Y<BR> 423 | 3057J<BR> 424 | 3057L<BR> 425 | 3057P<BR> 426 | 3057Y<BR> 427 | 3059J<BR> 428 | 3059L<BR> 429 | 3059P<BR> 430 | 3059Y<BR></FONT> 431 | </TD> 432 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 433 | -<BR> 434 | 89P<BR> 435 | 89W<BR> 436 | 89X<BR> 437 | 89PH<BR> 438 | 76P<BR> 439 | 89XH<BR> 440 | 78SLT<BR> 441 | 78L&nbsp;ALT<BR> 442 | 56P&nbsp;ALT<BR> 443 | 78P&nbsp;ALT<BR> 444 | T8S<BR> 445 | 78L<BR> 446 | 56P<BR> 447 | 78P<BR></FONT> 448 | </TD> 449 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 450 | -<BR> 451 | T18/784<BR> 452 | 783<BR> 453 | 781<BR> 454 | -<BR> 455 | -<BR> 456 | -<BR> 457 | 2199<BR> 458 | 1697/1897<BR> 459 | 1680/1880<BR> 460 | 2187<BR> 461 | -<BR> 462 | -<BR> 463 | -<BR> 464 | -<BR></FONT> 465 | </TD> 466 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 467 | -<BR> 468 | 8035EKP/CT20/RJ-20P<BR> 469 | -<BR> 470 | RJ-20X<BR> 471 | -<BR> 472 | -<BR> 473 | -<BR> 474 | 1211L<BR> 475 | 8012EKQ&nbsp;ALT<BR> 476 | 8012EKR&nbsp;ALT<BR> 477 | 1211P<BR> 478 | 8012EKJ<BR> 479 | 8012EKL<BR> 480 | 8012EKQ<BR> 481 | 8012EKR<BR></FONT> 482 | </TD> 483 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 484 | -<BR> 485 | 2101P<BR> 486 | 2101W<BR> 487 | 2101Y<BR> 488 | -<BR> 489 | -<BR> 490 | -<BR> 491 | -<BR> 492 | -<BR> 493 | -<BR> 494 | -<BR> 495 | -<BR> 496 | 2102L<BR> 497 | 2102S<BR> 498 | 2102Y<BR></FONT> 499 | </TD> 500 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 501 | -<BR> 502 | EVMCOG<BR> 503 | -<BR> 504 | -<BR> 505 | -<BR> 506 | -<BR> 507 | -<BR> 508 | -<BR> 509 | -<BR> 510 | -<BR> 511 | -<BR> 512 | -<BR> 513 | -<BR> 514 | -<BR> 515 | -<BR></FONT> 516 | </TD> 517 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 518 | -<BR> 519 | 43P<BR> 520 | 43W<BR> 521 | 43Y<BR> 522 | -<BR> 523 | -<BR> 524 | -<BR> 525 | -<BR> 526 | 40L<BR> 527 | 40P<BR> 528 | 40Y<BR> 529 | 70Y-T602<BR> 530 | 70L<BR> 531 | 70P<BR> 532 | 70Y<BR></FONT> 533 | </TD> 534 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 535 | -<BR> 536 | -<BR> 537 | -<BR> 538 | -<BR> 539 | -<BR> 540 | -<BR> 541 | -<BR> 542 | -<BR> 543 | RT/RTR12<BR> 544 | RT/RTR12<BR> 545 | RT/RTR12<BR> 546 | -<BR> 547 | RJ/RJR12<BR> 548 | RJ/RJR12<BR> 549 | RJ/RJR12<BR></FONT> 550 | </TD> 551 | </TR> 552 | <TR> 553 | <TD COLSPAN=8>&nbsp; 554 | </TD> 555 | </TR> 556 | <TR> 557 | <TD COLSPAN=8> 558 | <FONT SIZE=4 FACE=ARIAL><B>SQUARE MULTI-TURN</B></FONT> 559 | </TD> 560 | </TR> 561 | <TR> 562 | <TD ALIGN=CENTER> 563 | <FONT SIZE=3 FACE=ARIAL><B>BOURN</B></FONT> 564 | </TD> 565 | <TD ALIGN=CENTER> 566 | <FONT SIZE=3 FACE=ARIAL><B>BI&nbsp;TECH</B></FONT> 567 | </TD> 568 | <TD ALIGN=CENTER> 569 | <FONT SIZE=3 FACE=ARIAL><B>DALE-VISHAY</B></FONT> 570 | </TD> 571 | <TD ALIGN=CENTER> 572 | <FONT SIZE=3 FACE=ARIAL><B>PHILIPS/MEPCO</B></FONT> 573 | </TD> 574 | <TD ALIGN=CENTER> 575 | <FONT SIZE=3 FACE=ARIAL><B>MURATA</B></FONT> 576 | </TD> 577 | <TD ALIGN=CENTER> 578 | <FONT SIZE=3 FACE=ARIAL><B>PANASONIC</B></FONT> 579 | </TD> 580 | <TD ALIGN=CENTER> 581 | <FONT SIZE=3 FACE=ARIAL><B>SPECTROL</B></FONT> 582 | </TD> 583 | <TD ALIGN=CENTER> 584 | <FONT SIZE=3 FACE=ARIAL><B>MILSPEC</B></FONT> 585 | </TD> 586 | </TR> 587 | <TR> 588 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 589 | 3250L<BR> 590 | 3250P<BR> 591 | 3250W<BR> 592 | 3250X<BR> 593 | 3252P<BR> 594 | 3252W<BR> 595 | 3252X<BR> 596 | 3260P<BR> 597 | 3260W<BR> 598 | 3260X<BR> 599 | 3262P<BR> 600 | 3262W<BR> 601 | 3262X<BR> 602 | 3266P<BR> 603 | 3266W<BR> 604 | 3266X<BR> 605 | 3290H<BR> 606 | 3290P<BR> 607 | 3290W<BR> 608 | 3292P<BR> 609 | 3292W<BR> 610 | 3292X<BR> 611 | 3296P<BR> 612 | 3296W<BR> 613 | 3296X<BR> 614 | 3296Y<BR> 615 | 3296Z<BR> 616 | 3299P<BR> 617 | 3299W<BR> 618 | 3299X<BR> 619 | 3299Y<BR> 620 | 3299Z<BR></FONT> 621 | </TD> 622 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 623 | -<BR> 624 | 66P&nbsp;ALT<BR> 625 | 66W&nbsp;ALT<BR> 626 | 66X&nbsp;ALT<BR> 627 | 66P&nbsp;ALT<BR> 628 | 66W&nbsp;ALT<BR> 629 | 66X&nbsp;ALT<BR> 630 | -<BR> 631 | 64W&nbsp;ALT<BR> 632 | -<BR> 633 | 64P&nbsp;ALT<BR> 634 | 64W&nbsp;ALT<BR> 635 | 64X&nbsp;ALT<BR> 636 | 64P<BR> 637 | 64W<BR> 638 | 64X<BR> 639 | 66X&nbsp;ALT<BR> 640 | 66P&nbsp;ALT<BR> 641 | 66W&nbsp;ALT<BR> 642 | 66P<BR> 643 | 66W<BR> 644 | 66X<BR> 645 | 67P<BR> 646 | 67W<BR> 647 | 67X<BR> 648 | 67Y<BR> 649 | 67Z<BR> 650 | 68P<BR> 651 | 68W<BR> 652 | 68X<BR> 653 | 67Y&nbsp;ALT<BR> 654 | 67Z&nbsp;ALT<BR></FONT> 655 | </TD> 656 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 657 | 5050<BR> 658 | 5091<BR> 659 | 5080<BR> 660 | 5087<BR> 661 | -<BR> 662 | -<BR> 663 | -<BR> 664 | -<BR> 665 | -<BR> 666 | -<BR> 667 | -<BR> 668 | T63YB<BR> 669 | T63XB<BR> 670 | -<BR> 671 | -<BR> 672 | -<BR> 673 | 5887<BR> 674 | 5891<BR> 675 | 5880<BR> 676 | -<BR> 677 | -<BR> 678 | -<BR> 679 | T93Z<BR> 680 | T93YA<BR> 681 | T93XA<BR> 682 | T93YB<BR> 683 | T93XB<BR> 684 | -<BR> 685 | -<BR> 686 | -<BR> 687 | -<BR> 688 | -<BR></FONT> 689 | </TD> 690 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 691 | -<BR> 692 | -<BR> 693 | -<BR> 694 | -<BR> 695 | -<BR> 696 | -<BR> 697 | -<BR> 698 | -<BR> 699 | -<BR> 700 | -<BR> 701 | 8026EKP<BR> 702 | 8026EKW<BR> 703 | 8026EKM<BR> 704 | 8026EKP<BR> 705 | 8026EKB<BR> 706 | 8026EKM<BR> 707 | 1309X<BR> 708 | 1309P<BR> 709 | 1309W<BR> 710 | 8024EKP<BR> 711 | 8024EKW<BR> 712 | 8024EKN<BR> 713 | RJ-9P/CT9P<BR> 714 | RJ-9W<BR> 715 | RJ-9X<BR> 716 | -<BR> 717 | -<BR> 718 | -<BR> 719 | -<BR> 720 | -<BR> 721 | -<BR> 722 | -<BR></FONT> 723 | </TD> 724 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 725 | -<BR> 726 | -<BR> 727 | -<BR> 728 | -<BR> 729 | -<BR> 730 | -<BR> 731 | -<BR> 732 | -<BR> 733 | -<BR> 734 | -<BR> 735 | 3103P<BR> 736 | 3103Y<BR> 737 | 3103Z<BR> 738 | 3103P<BR> 739 | 3103Y<BR> 740 | 3103Z<BR> 741 | -<BR> 742 | -<BR> 743 | -<BR> 744 | -<BR> 745 | -<BR> 746 | -<BR> 747 | 3105P/3106P<BR> 748 | 3105W/3106W<BR> 749 | 3105X/3106X<BR> 750 | 3105Y/3106Y<BR> 751 | 3105Z/3105Z<BR> 752 | 3102P<BR> 753 | 3102W<BR> 754 | 3102X<BR> 755 | 3102Y<BR> 756 | 3102Z<BR></FONT> 757 | </TD> 758 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 759 | -<BR> 760 | -<BR> 761 | -<BR> 762 | -<BR> 763 | -<BR> 764 | -<BR> 765 | -<BR> 766 | -<BR> 767 | -<BR> 768 | -<BR> 769 | -<BR> 770 | -<BR> 771 | -<BR> 772 | -<BR> 773 | -<BR> 774 | -<BR> 775 | -<BR> 776 | -<BR> 777 | -<BR> 778 | -<BR> 779 | -<BR> 780 | -<BR> 781 | EVMCBG<BR> 782 | EVMCCG<BR> 783 | -<BR> 784 | -<BR> 785 | -<BR> 786 | -<BR> 787 | -<BR> 788 | -<BR> 789 | -<BR> 790 | -<BR></FONT> 791 | </TD> 792 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 793 | 55-1-X<BR> 794 | 55-4-X<BR> 795 | 55-3-X<BR> 796 | 55-2-X<BR> 797 | -<BR> 798 | -<BR> 799 | -<BR> 800 | -<BR> 801 | -<BR> 802 | -<BR> 803 | -<BR> 804 | -<BR> 805 | -<BR> 806 | -<BR> 807 | -<BR> 808 | -<BR> 809 | 50-2-X<BR> 810 | 50-4-X<BR> 811 | 50-3-X<BR> 812 | -<BR> 813 | -<BR> 814 | -<BR> 815 | 64P<BR> 816 | 64W<BR> 817 | 64X<BR> 818 | 64Y<BR> 819 | 64Z<BR> 820 | -<BR> 821 | -<BR> 822 | -<BR> 823 | -<BR> 824 | -<BR></FONT> 825 | </TD> 826 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 827 | RT/RTR22<BR> 828 | RT/RTR22<BR> 829 | RT/RTR22<BR> 830 | RT/RTR22<BR> 831 | RJ/RJR22<BR> 832 | RJ/RJR22<BR> 833 | RJ/RJR22<BR> 834 | RT/RTR26<BR> 835 | RT/RTR26<BR> 836 | RT/RTR26<BR> 837 | RJ/RJR26<BR> 838 | RJ/RJR26<BR> 839 | RJ/RJR26<BR> 840 | RJ/RJR26<BR> 841 | RJ/RJR26<BR> 842 | RJ/RJR26<BR> 843 | RT/RTR24<BR> 844 | RT/RTR24<BR> 845 | RT/RTR24<BR> 846 | RJ/RJR24<BR> 847 | RJ/RJR24<BR> 848 | RJ/RJR24<BR> 849 | RJ/RJR24<BR> 850 | RJ/RJR24<BR> 851 | RJ/RJR24<BR> 852 | -<BR> 853 | -<BR> 854 | -<BR> 855 | -<BR> 856 | -<BR> 857 | -<BR> 858 | -<BR></FONT> 859 | </TD> 860 | </TR> 861 | <TR> 862 | <TD COLSPAN=8>&nbsp; 863 | </TD> 864 | </TR> 865 | <TR> 866 | <TD COLSPAN=8> 867 | <FONT SIZE=4 FACE=ARIAL><B>SINGLE TURN</B></FONT> 868 | </TD> 869 | </TR> 870 | <TR> 871 | <TD ALIGN=CENTER> 872 | <FONT SIZE=3 FACE=ARIAL><B>BOURN</B></FONT> 873 | </TD> 874 | <TD ALIGN=CENTER> 875 | <FONT SIZE=3 FACE=ARIAL><B>BI&nbsp;TECH</B></FONT> 876 | </TD> 877 | <TD ALIGN=CENTER> 878 | <FONT SIZE=3 FACE=ARIAL><B>DALE-VISHAY</B></FONT> 879 | </TD> 880 | <TD ALIGN=CENTER> 881 | <FONT SIZE=3 FACE=ARIAL><B>PHILIPS/MEPCO</B></FONT> 882 | </TD> 883 | <TD ALIGN=CENTER> 884 | <FONT SIZE=3 FACE=ARIAL><B>MURATA</B></FONT> 885 | </TD> 886 | <TD ALIGN=CENTER> 887 | <FONT SIZE=3 FACE=ARIAL><B>PANASONIC</B></FONT> 888 | </TD> 889 | <TD ALIGN=CENTER> 890 | <FONT SIZE=3 FACE=ARIAL><B>SPECTROL</B></FONT> 891 | </TD> 892 | <TD ALIGN=CENTER> 893 | <FONT SIZE=3 FACE=ARIAL><B>MILSPEC</B></FONT> 894 | </TD> 895 | </TR> 896 | <TR> 897 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 898 | 3323P<BR> 899 | 3323S<BR> 900 | 3323W<BR> 901 | 3329H<BR> 902 | 3329P<BR> 903 | 3329W<BR> 904 | 3339H<BR> 905 | 3339P<BR> 906 | 3339W<BR> 907 | 3352E<BR> 908 | 3352H<BR> 909 | 3352K<BR> 910 | 3352P<BR> 911 | 3352T<BR> 912 | 3352V<BR> 913 | 3352W<BR> 914 | 3362H<BR> 915 | 3362M<BR> 916 | 3362P<BR> 917 | 3362R<BR> 918 | 3362S<BR> 919 | 3362U<BR> 920 | 3362W<BR> 921 | 3362X<BR> 922 | 3386B<BR> 923 | 3386C<BR> 924 | 3386F<BR> 925 | 3386H<BR> 926 | 3386K<BR> 927 | 3386M<BR> 928 | 3386P<BR> 929 | 3386S<BR> 930 | 3386W<BR> 931 | 3386X<BR></FONT> 932 | </TD> 933 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 934 | 25P<BR> 935 | 25S<BR> 936 | 25RX<BR> 937 | 82P<BR> 938 | 82M<BR> 939 | 82PA<BR> 940 | -<BR> 941 | -<BR> 942 | -<BR> 943 | 91E<BR> 944 | 91X<BR> 945 | 91T<BR> 946 | 91B<BR> 947 | 91A<BR> 948 | 91V<BR> 949 | 91W<BR> 950 | 25W<BR> 951 | 25V<BR> 952 | 25P<BR> 953 | -<BR> 954 | 25S<BR> 955 | 25U<BR> 956 | 25RX<BR> 957 | 25X<BR> 958 | 72XW<BR> 959 | 72XL<BR> 960 | 72PM<BR> 961 | 72RX<BR> 962 | -<BR> 963 | 72PX<BR> 964 | 72P<BR> 965 | 72RXW<BR> 966 | 72RXL<BR> 967 | 72X<BR></FONT> 968 | </TD> 969 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 970 | -<BR> 971 | -<BR> 972 | -<BR> 973 | T7YB<BR> 974 | T7YA<BR> 975 | -<BR> 976 | -<BR> 977 | -<BR> 978 | -<BR> 979 | -<BR> 980 | -<BR> 981 | -<BR> 982 | -<BR> 983 | -<BR> 984 | -<BR> 985 | -<BR> 986 | -<BR> 987 | TXD<BR> 988 | TYA<BR> 989 | TYP<BR> 990 | -<BR> 991 | TYD<BR> 992 | TX<BR> 993 | -<BR> 994 | 150SX<BR> 995 | 100SX<BR> 996 | 102T<BR> 997 | 101S<BR> 998 | 190T<BR> 999 | 150TX<BR> 1000 | 101<BR> 1001 | -<BR> 1002 | -<BR> 1003 | 101SX<BR></FONT> 1004 | </TD> 1005 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 1006 | ET6P<BR> 1007 | ET6S<BR> 1008 | ET6X<BR> 1009 | RJ-6W/8014EMW<BR> 1010 | RJ-6P/8014EMP<BR> 1011 | RJ-6X/8014EMX<BR> 1012 | TM7W<BR> 1013 | TM7P<BR> 1014 | TM7X<BR> 1015 | -<BR> 1016 | 8017SMS<BR> 1017 | -<BR> 1018 | 8017SMB<BR> 1019 | 8017SMA<BR> 1020 | -<BR> 1021 | -<BR> 1022 | CT-6W<BR> 1023 | CT-6H<BR> 1024 | CT-6P<BR> 1025 | CT-6R<BR> 1026 | -<BR> 1027 | CT-6V<BR> 1028 | CT-6X<BR> 1029 | -<BR> 1030 | -<BR> 1031 | 8038EKV<BR> 1032 | -<BR> 1033 | 8038EKX<BR> 1034 | -<BR> 1035 | -<BR> 1036 | 8038EKP<BR> 1037 | 8038EKZ<BR> 1038 | 8038EKW<BR> 1039 | -<BR></FONT> 1040 | </TD> 1041 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 1042 | -<BR> 1043 | -<BR> 1044 | -<BR> 1045 | 3321H<BR> 1046 | 3321P<BR> 1047 | 3321N<BR> 1048 | 1102H<BR> 1049 | 1102P<BR> 1050 | 1102T<BR> 1051 | RVA0911V304A<BR> 1052 | -<BR> 1053 | RVA0911H413A<BR> 1054 | RVG0707V100A<BR> 1055 | RVA0607V(H)306A<BR> 1056 | RVA1214H213A<BR> 1057 | -<BR> 1058 | -<BR> 1059 | -<BR> 1060 | -<BR> 1061 | -<BR> 1062 | -<BR> 1063 | -<BR> 1064 | -<BR> 1065 | -<BR> 1066 | 3104B<BR> 1067 | 3104C<BR> 1068 | 3104F<BR> 1069 | 3104H<BR> 1070 | -<BR> 1071 | 3104M<BR> 1072 | 3104P<BR> 1073 | 3104S<BR> 1074 | 3104W<BR> 1075 | 3104X<BR></FONT> 1076 | </TD> 1077 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 1078 | EVMQ0G<BR> 1079 | EVMQIG<BR> 1080 | EVMQ3G<BR> 1081 | EVMS0G<BR> 1082 | EVMQ0G<BR> 1083 | EVMG0G<BR> 1084 | -<BR> 1085 | -<BR> 1086 | -<BR> 1087 | EVMK4GA00B<BR> 1088 | EVM30GA00B<BR> 1089 | EVMK0GA00B<BR> 1090 | EVM38GA00B<BR> 1091 | EVMB6<BR> 1092 | EVLQ0<BR> 1093 | -<BR> 1094 | EVMMSG<BR> 1095 | EVMMBG<BR> 1096 | EVMMAG<BR> 1097 | -<BR> 1098 | -<BR> 1099 | EVMMCS<BR> 1100 | -<BR> 1101 | -<BR> 1102 | -<BR> 1103 | -<BR> 1104 | -<BR> 1105 | EVMM1<BR> 1106 | -<BR> 1107 | -<BR> 1108 | EVMM0<BR> 1109 | -<BR> 1110 | -<BR> 1111 | EVMM3<BR></FONT> 1112 | </TD> 1113 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 1114 | -<BR> 1115 | -<BR> 1116 | -<BR> 1117 | 62-3-1<BR> 1118 | 62-1-2<BR> 1119 | -<BR> 1120 | -<BR> 1121 | -<BR> 1122 | -<BR> 1123 | -<BR> 1124 | -<BR> 1125 | -<BR> 1126 | -<BR> 1127 | -<BR> 1128 | -<BR> 1129 | -<BR> 1130 | 67R<BR> 1131 | -<BR> 1132 | 67P<BR> 1133 | -<BR> 1134 | -<BR> 1135 | -<BR> 1136 | -<BR> 1137 | 67X<BR> 1138 | 63V<BR> 1139 | 63S<BR> 1140 | 63M<BR> 1141 | -<BR> 1142 | -<BR> 1143 | 63H<BR> 1144 | 63P<BR> 1145 | -<BR> 1146 | -<BR> 1147 | 63X<BR></FONT> 1148 | </TD> 1149 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 1150 | -<BR> 1151 | -<BR> 1152 | -<BR> 1153 | RJ/RJR50<BR> 1154 | RJ/RJR50<BR> 1155 | RJ/RJR50<BR> 1156 | -<BR> 1157 | -<BR> 1158 | -<BR> 1159 | -<BR> 1160 | -<BR> 1161 | -<BR> 1162 | -<BR> 1163 | -<BR> 1164 | -<BR> 1165 | -<BR> 1166 | -<BR> 1167 | -<BR> 1168 | -<BR> 1169 | -<BR> 1170 | -<BR> 1171 | -<BR> 1172 | -<BR> 1173 | -<BR> 1174 | -<BR> 1175 | -<BR> 1176 | -<BR> 1177 | -<BR> 1178 | -<BR> 1179 | -<BR> 1180 | -<BR> 1181 | -<BR> 1182 | -<BR> 1183 | -<BR></FONT> 1184 | </TD> 1185 | </TR> 1186 | </TABLE> 1187 | <P>&nbsp;<P> 1188 | <TABLE BORDER=0 CELLSPACING=1 CELLPADDING=3> 1189 | <TR> 1190 | <TD COLSPAN=7> 1191 | <FONT color="#0000FF" SIZE=4 FACE=ARIAL><B>SMD TRIM-POT CROSS REFERENCE</B></FONT> 1192 | <P> 1193 | <FONT SIZE=4 FACE=ARIAL><B>MULTI-TURN</B></FONT> 1194 | </TD> 1195 | </TR> 1196 | <TR> 1197 | <TD> 1198 | <FONT SIZE=3 FACE=ARIAL><B>BOURNS</B></FONT> 1199 | </TD> 1200 | <TD> 1201 | <FONT SIZE=3 FACE=ARIAL><B>BI&nbsp;TECH</B></FONT> 1202 | </TD> 1203 | <TD> 1204 | <FONT SIZE=3 FACE=ARIAL><B>DALE-VISHAY</B></FONT> 1205 | </TD> 1206 | <TD> 1207 | <FONT SIZE=3 FACE=ARIAL><B>PHILIPS/MEPCO</B></FONT> 1208 | </TD> 1209 | <TD> 1210 | <FONT SIZE=3 FACE=ARIAL><B>PANASONIC</B></FONT> 1211 | </TD> 1212 | <TD> 1213 | <FONT SIZE=3 FACE=ARIAL><B>TOCOS</B></FONT> 1214 | </TD> 1215 | <TD> 1216 | <FONT SIZE=3 FACE=ARIAL><B>AUX/KYOCERA</B></FONT> 1217 | </TD> 1218 | </TR> 1219 | <TR> 1220 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 1221 | 3224G<BR> 1222 | 3224J<BR> 1223 | 3224W<BR> 1224 | 3269P<BR> 1225 | 3269W<BR> 1226 | 3269X<BR></FONT> 1227 | </TD> 1228 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 1229 | 44G<BR> 1230 | 44J<BR> 1231 | 44W<BR> 1232 | 84P<BR> 1233 | 84W<BR> 1234 | 84X<BR></FONT> 1235 | </TD> 1236 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 1237 | -<BR> 1238 | -<BR> 1239 | -<BR> 1240 | ST63Z<BR> 1241 | ST63Y<BR> 1242 | -<BR></FONT> 1243 | </TD> 1244 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 1245 | -<BR> 1246 | -<BR> 1247 | -<BR> 1248 | ST5P<BR> 1249 | ST5W<BR> 1250 | ST5X<BR></FONT> 1251 | </TD> 1252 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 1253 | -<BR> 1254 | -<BR> 1255 | -<BR> 1256 | -<BR> 1257 | -<BR> 1258 | -<BR></FONT> 1259 | </TD> 1260 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 1261 | -<BR> 1262 | -<BR> 1263 | -<BR> 1264 | -<BR> 1265 | -<BR> 1266 | -<BR></FONT> 1267 | </TD> 1268 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 1269 | -<BR> 1270 | -<BR> 1271 | -<BR> 1272 | -<BR> 1273 | -<BR> 1274 | -<BR></FONT> 1275 | </TD> 1276 | </TR> 1277 | <TR> 1278 | <TD COLSPAN=7>&nbsp; 1279 | </TD> 1280 | </TR> 1281 | <TR> 1282 | <TD COLSPAN=7> 1283 | <FONT SIZE=4 FACE=ARIAL><B>SINGLE TURN</B></FONT> 1284 | </TD> 1285 | </TR> 1286 | <TR> 1287 | <TD> 1288 | <FONT SIZE=3 FACE=ARIAL><B>BOURNS</B></FONT> 1289 | </TD> 1290 | <TD> 1291 | <FONT SIZE=3 FACE=ARIAL><B>BI&nbsp;TECH</B></FONT> 1292 | </TD> 1293 | <TD> 1294 | <FONT SIZE=3 FACE=ARIAL><B>DALE-VISHAY</B></FONT> 1295 | </TD> 1296 | <TD> 1297 | <FONT SIZE=3 FACE=ARIAL><B>PHILIPS/MEPCO</B></FONT> 1298 | </TD> 1299 | <TD> 1300 | <FONT SIZE=3 FACE=ARIAL><B>PANASONIC</B></FONT> 1301 | </TD> 1302 | <TD> 1303 | <FONT SIZE=3 FACE=ARIAL><B>TOCOS</B></FONT> 1304 | </TD> 1305 | <TD> 1306 | <FONT SIZE=3 FACE=ARIAL><B>AUX/KYOCERA</B></FONT> 1307 | </TD> 1308 | </TR> 1309 | <TR> 1310 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 1311 | 3314G<BR> 1312 | 3314J<BR> 1313 | 3364A/B<BR> 1314 | 3364C/D<BR> 1315 | 3364W/X<BR> 1316 | 3313G<BR> 1317 | 3313J<BR></FONT> 1318 | </TD> 1319 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 1320 | 23B<BR> 1321 | 23A<BR> 1322 | 21X<BR> 1323 | 21W<BR> 1324 | -<BR> 1325 | 22B<BR> 1326 | 22A<BR></FONT> 1327 | </TD> 1328 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 1329 | ST5YL/ST53YL<BR> 1330 | ST5YJ/5T53YJ<BR> 1331 | ST-23A<BR> 1332 | ST-22B<BR> 1333 | ST-22<BR> 1334 | -<BR> 1335 | -<BR></FONT> 1336 | </TD> 1337 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 1338 | ST-4B<BR> 1339 | ST-4A<BR> 1340 | -<BR> 1341 | -<BR> 1342 | -<BR> 1343 | ST-3B<BR> 1344 | ST-3A<BR></FONT> 1345 | </TD> 1346 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 1347 | -<BR> 1348 | EVM-6YS<BR> 1349 | EVM-1E<BR> 1350 | EVM-1G<BR> 1351 | EVM-1D<BR> 1352 | -<BR> 1353 | -<BR></FONT> 1354 | </TD> 1355 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 1356 | G4B<BR> 1357 | G4A<BR> 1358 | TR04-3S1<BR> 1359 | TRG04-2S1<BR> 1360 | -<BR> 1361 | -<BR> 1362 | -<BR></FONT> 1363 | </TD> 1364 | <TD BGCOLOR="#cccccc" ALIGN=CENTER><FONT FACE=ARIAL SIZE=3> 1365 | -<BR> 1366 | -<BR> 1367 | DVR-43A<BR> 1368 | CVR-42C<BR> 1369 | CVR-42A/C<BR> 1370 | -<BR> 1371 | -<BR></FONT> 1372 | </TD> 1373 | </TR> 1374 | </TABLE> 1375 | <P> 1376 | <FONT SIZE=4 FACE=ARIAL><B>ALT =&nbsp;ALTERNATE</B></FONT> 1377 | <P> 1378 | 1379 | &nbsp; 1380 | <P> 1381 | </td> 1382 | </tr> 1383 | </table> 1384 | 1385 | 1386 | <b>RESISTOR</b><p> 1387 | 1388 | 1389 | 1390 | 1391 | 1392 | 1393 | 1394 | 1395 | >NAME 1396 | >VALUE 1397 | 1398 | 1399 | 1400 | 1401 | 1402 | <b>CAPACITOR</b><p> 1403 | 1404 | 1405 | 1406 | 1407 | 1408 | 1409 | 1410 | 1411 | >NAME 1412 | >VALUE 1413 | 1414 | 1415 | 1416 | 1417 | 1418 | 1419 | 1420 | RESISTOR 1421 | 1422 | 1423 | 1424 | 1425 | 1426 | CAPACITOR 1427 | 1428 | 1429 | 1430 | 1431 | 1432 | 1433 | 1434 | <b>Opto Couplers</b><p> 1435 | Siemens, Hewlett-Packard, Texas Instuments, Sharp, Motorola<p> 1436 | <author>Created by librarian@cadsoft.de</author> 1437 | 1438 | 1439 | <b>DIL 6 SMD</b><p> 1440 | Source: http://www.fairchildsemi.com/pf/4N/4N35-M.html 1441 | 1442 | 1443 | 1444 | 1445 | 1446 | 1447 | 1448 | 1449 | 1450 | 1451 | 1452 | 1453 | 1454 | >NAME 1455 | >VALUE 1456 | 1457 | 1458 | 1459 | 1460 | 1461 | 1462 | 1463 | 1464 | 1465 | 1466 | DIL 6 SMD 1467 | Source: http://www.fairchildsemi.com/pf/4N/4N35-M.html 1468 | 1469 | 1470 | 1471 | 1472 | 1473 | 1474 | 1475 | <b>Diodes</b><p> 1476 | Based on the following sources: 1477 | <ul> 1478 | <li>Motorola : www.onsemi.com 1479 | <li>Fairchild : www.fairchildsemi.com 1480 | <li>Philips : www.semiconductors.com 1481 | <li>Vishay : www.vishay.de 1482 | </ul> 1483 | <author>Created by librarian@cadsoft.de</author> 1484 | 1485 | 1486 | <b>Diode</b> 1487 | 1488 | 1489 | 1490 | 1491 | 1492 | 1493 | >NAME 1494 | >VALUE 1495 | 1496 | 1497 | 1498 | 1499 | 1500 | 1501 | 1502 | Diode 1503 | 1504 | 1505 | 1506 | 1507 | 1508 | 1509 | 1510 | 1511 | 1512 | 1513 | 1514 | 1515 | 1516 | 1517 | 1518 | 1519 | 1520 | 1521 | 1522 | 1523 | 1524 | 1525 | 1526 | 1527 | 1528 | 1529 | 1530 | 1531 | 1532 | 1533 | 1534 | 1535 | 1536 | 1537 | 1538 | 1539 | 1540 | 1541 | 1542 | 1543 | 1544 | 1545 | 1546 | 1547 | 1548 | 1549 | 1550 | 1551 | 1552 | 1553 | 1554 | 1555 | 1556 | 1557 | 1558 | 1559 | 1560 | 1561 | 1562 | 1563 | 1564 | 1565 | 1566 | 1567 | 1568 | 1569 | 1570 | 1571 | 1572 | 1573 | 1574 | 1575 | 1576 | 1577 | A0 1578 | A1 1579 | A2 1580 | A3 1581 | SDA 1582 | SCL 1583 | TX 1584 | RX 1585 | SCK 1586 | MI 1587 | MO 1588 | 3V 1589 | GND 1590 | 5V 1591 | QT PY 1592 | USB 1593 | 1594 | 1595 | 1596 | >Name 1597 | 1598 | 1599 | 1600 | 1601 | 1602 | 1603 | 1604 | 1605 | 1606 | 1607 | 1608 | 1609 | 1610 | 1611 | 1612 | 1613 | 1614 | 1615 | 1616 | 1617 | 1618 | 1619 | 1620 | 1621 | 1622 | 1623 | 1624 | 1625 | 1626 | 1627 | 1628 | 1629 | 1630 | 1631 | 1632 | 1633 | 1634 | 1635 | >NAME 1636 | >VALUE 1637 | 1638 | 1639 | 1640 | 1641 | GND 1642 | VCC 1643 | SCL 1644 | SDA 1645 | 1646 | 1647 | 1648 | 1649 | 1650 | 1651 | 1652 | 1653 | 1654 | 1655 | 1656 | 1657 | 1658 | 1659 | 1660 | 1661 | 1662 | 1663 | 1664 | 1665 | 1666 | 1667 | 1668 | 1669 | 1670 | 1671 | 1672 | 1673 | 1674 | 1675 | 1676 | 1677 | 1678 | 1679 | 1680 | >VALUE 1681 | 1682 | 1683 | >DESC 1684 | 1685 | 1686 | >NAME 1687 | >VALUE 1688 | 1689 | 1690 | 1691 | 1692 | 1693 | <b>Omron Switches</b><p> 1694 | <author>Created by librarian@cadsoft.de</author> 1695 | 1696 | 1697 | <b>OMRON SWITCH</b> 1698 | 1699 | 1700 | 1701 | 1702 | 1703 | 1704 | 1705 | 1706 | 1707 | 1708 | 1709 | 1710 | 1711 | 1712 | 1713 | 1714 | 1715 | 1716 | 1717 | 1718 | 1719 | 1720 | 1721 | 1722 | 1723 | 1724 | 1725 | 1726 | 1727 | 1728 | 1729 | 1730 | 1731 | 1732 | 1733 | 1734 | 1735 | 1736 | 1737 | 1738 | 1739 | 1740 | 1741 | 1742 | 1743 | 1744 | 1745 | 1746 | 1747 | 1748 | 1749 | 1750 | 1751 | 1752 | >NAME 1753 | >VALUE 1754 | 1 1755 | 2 1756 | 3 1757 | 4 1758 | 1759 | 1760 | 1761 | 1762 | OMRON SWITCH 1763 | 1764 | 1765 | 1766 | 1767 | 1768 | 1769 | 1770 | 1771 | 1772 | 1773 | 1774 | 1775 | 1776 | 1777 | 1778 | 1779 | 1780 | 1781 | 1782 | 1783 | 1784 | <b>EAGLE Design Rules</b> 1785 | <p> 1786 | Die Standard-Design-Rules sind so gewählt, dass sie für 1787 | die meisten Anwendungen passen. Sollte ihre Platine 1788 | besondere Anforderungen haben, treffen Sie die erforderlichen 1789 | Einstellungen hier und speichern die Design Rules unter 1790 | einem neuen Namen ab. 1791 | <b>Adafruit board design rules</b> 1792 | <p> 1793 | <ul> 1794 | <li>Smallest drill: 16mm</li> 1795 | <li>Min trace: 10mil</li> 1796 | <li>Min spacing: 8mil</li> 1797 | <li>Dimension spacing: 10mil</li> 1798 | <li>Tenting for vias</li> 1799 | <li>Angle check on</li> 1800 | </ul> 1801 | 1802 | 1803 | 1804 | 1805 | 1806 | 1807 | 1808 | 1809 | 1810 | 1811 | 1812 | 1813 | 1814 | 1815 | 1816 | 1817 | 1818 | 1819 | 1820 | 1821 | 1822 | 1823 | 1824 | 1825 | 1826 | 1827 | 1828 | 1829 | 1830 | 1831 | 1832 | 1833 | 1834 | 1835 | 1836 | 1837 | 1838 | 1839 | 1840 | 1841 | 1842 | 1843 | 1844 | 1845 | 1846 | 1847 | 1848 | 1849 | 1850 | 1851 | 1852 | 1853 | 1854 | 1855 | 1856 | 1857 | 1858 | 1859 | 1860 | 1861 | 1862 | 1863 | 1864 | 1865 | 1866 | 1867 | 1868 | 1869 | 1870 | 1871 | 1872 | 1873 | 1874 | 1875 | 1876 | 1877 | 1878 | 1879 | 1880 | 1881 | 1882 | 1883 | 1884 | 1885 | 1886 | 1887 | 1888 | 1889 | 1890 | 1891 | 1892 | 1893 | 1894 | 1895 | 1896 | 1897 | 1898 | 1899 | 1900 | 1901 | 1902 | 1903 | 1904 | 1905 | 1906 | 1907 | 1908 | 1909 | 1910 | 1911 | 1912 | 1913 | 1914 | 1915 | 1916 | 1917 | 1918 | 1919 | 1920 | 1921 | 1922 | 1923 | 1924 | 1925 | 1926 | 1927 | 1928 | 1929 | 1930 | 1931 | 1932 | 1933 | 1934 | 1935 | 1936 | 1937 | 1938 | 1939 | 1940 | 1941 | 1942 | 1943 | 1944 | 1945 | 1946 | 1947 | 1948 | 1949 | 1950 | 1951 | 1952 | 1953 | 1954 | 1955 | 1956 | 1957 | 1958 | 1959 | 1960 | 1961 | 1962 | 1963 | 1964 | 1965 | 1966 | 1967 | 1968 | 1969 | 1970 | 1971 | 1972 | 1973 | 1974 | 1975 | 1976 | 1977 | 1978 | 1979 | 1980 | 1981 | 1982 | 1983 | 1984 | 1985 | 1986 | 1987 | 1988 | 1989 | 1990 | 1991 | 1992 | 1993 | 1994 | 1995 | 1996 | 1997 | 1998 | 1999 | 2000 | 2001 | 2002 | 2003 | 2004 | 2005 | 2006 | 2007 | 2008 | 2009 | 2010 | 2011 | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 | 2026 | 2027 | 2028 | 2029 | 2030 | 2031 | 2032 | 2033 | 2034 | 2035 | 2036 | 2037 | 2038 | 2039 | 2040 | 2041 | 2042 | 2043 | 2044 | 2045 | 2046 | 2047 | 2048 | 2049 | 2050 | 2051 | 2052 | 2053 | 2054 | 2055 | 2056 | 2057 | 2058 | 2059 | 2060 | 2061 | 2062 | 2063 | 2064 | 2065 | 2066 | 2067 | 2068 | 2069 | 2070 | 2071 | 2072 | 2073 | 2074 | 2075 | 2076 | 2077 | 2078 | 2079 | 2080 | 2081 | 2082 | 2083 | 2084 | 2085 | 2086 | 2087 | 2088 | 2089 | 2090 | 2091 | 2092 | 2093 | 2094 | 2095 | 2096 | 2097 | 2098 | 2099 | 2100 | 2101 | 2102 | 2103 | 2104 | 2105 | 2106 | 2107 | 2108 | 2109 | 2110 | 2111 | 2112 | 2113 | 2114 | 2115 | 2116 | 2117 | 2118 | 2119 | 2120 | 2121 | 2122 | 2123 | 2124 | 2125 | 2126 | 2127 | 2128 | 2129 | 2130 | 2131 | 2132 | 2133 | 2134 | 2135 | 2136 | 2137 | 2138 | 2139 | 2140 | 2141 | 2142 | 2143 | 2144 | 2145 | 2146 | 2147 | 2148 | 2149 | 2150 | 2151 | 2152 | 2153 | 2154 | 2155 | 2156 | 2157 | 2158 | 2159 | 2160 | 2161 | 2162 | 2163 | 2164 | 2165 | 2166 | 2167 | 2168 | 2169 | 2170 | 2171 | 2172 | 2173 | 2174 | 2175 | 2176 | 2177 | 2178 | 2179 | 2180 | 2181 | 2182 | 2183 | 2184 | 2185 | 2186 | 2187 | 2188 | 2189 | 2190 | 2191 | 2192 | 2193 | 2194 | 2195 | 2196 | 2197 | 2198 | 2199 | 2200 | 2201 | 2202 | 2203 | 2204 | 2205 | 2206 | 2207 | 2208 | 2209 | 2210 | 2211 | 2212 | 2213 | 2214 | 2215 | 2216 | 2217 | 2218 | 2219 | 2220 | 2221 | 2222 | 2223 | 2224 | 2225 | 2226 | 2227 | 2228 | 2229 | 2230 | 2231 | 2232 | 2233 | 2234 | 2235 | 2236 | 2237 | 2238 | 2239 | 2240 | 2241 | 2242 | 2243 | 2244 | 2245 | 2246 | 2247 | 2248 | 2249 | 2250 | 2251 | 2252 | 2253 | 2254 | 2255 | 2256 | 2257 | 2258 | 2259 | 2260 | 2261 | 2262 | 2263 | 2264 | 2265 | 2266 | 2267 | 2268 | 2269 | 2270 | 2271 | 2272 | 2273 | 2274 | 2275 | 2276 | 2277 | 2278 | 2279 | 2280 | 2281 | 2282 | 2283 | 2284 | 2285 | 2286 | 2287 | 2288 | 2289 | 2290 | 2291 | 2292 | 2293 | 2294 | 2295 | 2296 | 2297 | 2298 | 2299 | 2300 | 2301 | 2302 | 2303 | 2304 | 2305 | 2306 | 2307 | 2308 | 2309 | 2310 | 2311 | 2312 | 2313 | 2314 | 2315 | 2316 | 2317 | 2318 | 2319 | 2320 | 2321 | 2322 | 2323 | 2324 | 2325 | 2326 | 2327 | 2328 | 2329 | 2330 | 2331 | 2332 | 2333 | 2334 | 2335 | 2336 | 2337 | 2338 | 2339 | 2340 | 2341 | 2342 | 2343 | 2344 | 2345 | 2346 | 2347 | 2348 | 2349 | 2350 | 2351 | 2352 | 2353 | 2354 | 2355 | 2356 | 2357 | 2358 | 2359 | 2360 | 2361 | 2362 | 2363 | 2364 | 2365 | 2366 | 2367 | 2368 | Since Version 6.2.2 text objects can contain more than one line, 2369 | which will not be processed correctly with this version. 2370 | 2371 | 2372 | Since Version 8.2, EAGLE supports online libraries. The ids 2373 | of those online libraries will not be understood (or retained) 2374 | with this version. 2375 | 2376 | 2377 | Since Version 8.3, EAGLE supports Fusion synchronisation. 2378 | This feature will not be available in this version and saving 2379 | the document will break the link to the Fusion PCB feature. 2380 | 2381 | 2382 | Since Version 8.3, EAGLE supports URNs for individual library 2383 | assets (packages, symbols, and devices). The URNs of those assets 2384 | will not be understood (or retained) with this version. 2385 | 2386 | 2387 | Since Version 8.3, EAGLE supports the association of 3D packages 2388 | with devices in libraries, schematics, and board files. Those 3D 2389 | packages will not be understood (or retained) with this version. 2390 | 2391 | 2392 | Since Version 9.4, EAGLE supports the overriding of 3D packages 2393 | in schematics and board files. Those overridden 3d packages 2394 | will not be understood (or retained) with this version. 2395 | 2396 | 2397 | 2398 | -------------------------------------------------------------------------------- /schematics/qtpy_synth_proto2b_bom.txt: -------------------------------------------------------------------------------- 1 | Qty Value Device Package Parts Description LCSC MF MPN 2 | 1 10-XX B3F-10XX SW OMRON SWITCH B3F-1000 3 | 1 1.8k R-EU_R0805 R0805 R9 "RESISTOR, European symbol" 4 | 1 100 R-EU_R0805 R0805 R8 "RESISTOR, European symbol" 5 | 2 100n C-EUC0805 C0805 "C1, C2" "CAPACITOR, European symbol" 6 | 2 10k POTZEPHPOT_BIGKNOB OG-POTS_ALPS_POT_VERTICAL_WITHBIGKNOB "P1, P2" 7 | 4 1M R-EU_R0805 R0805 "R1, R2, R3, R4" "RESISTOR, European symbol" 8 | 1 1N4148 DIODE-SOD123 SOD123 D1 DIODE 9 | 2 220 R-EU_R0805 R0805 "R6, R7" "RESISTOR, European symbol" 10 | 1 4.70k R-EU_R0805 R0805 R5 "RESISTOR, European symbol" 11 | 1 47uF C-EUC0805 C0805 C3 "CAPACITOR, European symbol" 12 | 1 ADAFRUIT_QTPYM0 ADAFRUIT_QTPYM0 ADAFRUIT_QTPY_M0 U1 13 | 2 AUDIO-JACK-TRRS AUDIO-JACK-TRRS PJ-320D_SPARKFUN "J1, J2" C431535 PJ-320D 14 | 1 DISPLAY-OLED-128X64-I2CTOD DISPLAY-OLED-128X64-I2CTOD DISPLAY-OLED-128X64-I2C-TOD DISP 128x64 Dot Matrix OLED Module based on SSD1306 chip 15 | 1 H11L1MS H11L1MS DIL6-SMD IC1 6-Pin DIP Optocoupler C78589 -------------------------------------------------------------------------------- /schematics/qtpy_synth_proto2b_sch.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/qtpy_synth/e9ad4358de26e5973b7daa7514ee27f6ef0a1e8e/schematics/qtpy_synth_proto2b_sch.pdf -------------------------------------------------------------------------------- /schematics/qtpy_synth_proto2b_sch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/todbot/qtpy_synth/e9ad4358de26e5973b7daa7514ee27f6ef0a1e8e/schematics/qtpy_synth_proto2b_sch.png --------------------------------------------------------------------------------