├── .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 |
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 | [](https://www.youtube.com/watch?v=4hgDi6MNfsI)]
31 | - another video demo:
32 |
33 | [](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/).
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"> </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 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> </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 ALT<BR>
442 | 56P ALT<BR>
443 | 78P 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 ALT<BR>
476 | 8012EKR 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>
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 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 ALT<BR>
625 | 66W ALT<BR>
626 | 66X ALT<BR>
627 | 66P ALT<BR>
628 | 66W ALT<BR>
629 | 66X ALT<BR>
630 | -<BR>
631 | 64W ALT<BR>
632 | -<BR>
633 | 64P ALT<BR>
634 | 64W ALT<BR>
635 | 64X ALT<BR>
636 | 64P<BR>
637 | 64W<BR>
638 | 64X<BR>
639 | 66X ALT<BR>
640 | 66P ALT<BR>
641 | 66W 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 ALT<BR>
654 | 67Z 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>
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 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> <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 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>
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 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 = ALTERNATE</B></FONT>
1377 | <P>
1378 |
1379 |
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
--------------------------------------------------------------------------------