{"id":13981402,"url":"https://github.com/todbot/circuitpython-synthio-tricks","last_synced_at":"2025-09-06T22:38:05.469Z","repository":{"id":176478247,"uuid":"656357401","full_name":"todbot/circuitpython-synthio-tricks","owner":"todbot","description":"tips, tricks, and examples of using CircuitPython synthio","archived":false,"fork":false,"pushed_at":"2025-03-17T22:13:50.000Z","size":2021,"stargazers_count":85,"open_issues_count":1,"forks_count":6,"subscribers_count":11,"default_branch":"main","last_synced_at":"2025-08-28T16:51:14.314Z","etag":null,"topics":["circuitpython","i2s","raspberrypipico","rp2040","synth","synthdiy","synthesizer","synthio"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/todbot.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.TXT","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-06-20T19:35:20.000Z","updated_at":"2025-08-27T20:38:57.000Z","dependencies_parsed_at":"2024-03-28T20:51:46.321Z","dependency_job_id":"713cbe87-67c8-40c7-994e-57a370a17e7c","html_url":"https://github.com/todbot/circuitpython-synthio-tricks","commit_stats":null,"previous_names":["todbot/circuitpython-synthio-tricks"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/todbot/circuitpython-synthio-tricks","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/todbot%2Fcircuitpython-synthio-tricks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/todbot%2Fcircuitpython-synthio-tricks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/todbot%2Fcircuitpython-synthio-tricks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/todbot%2Fcircuitpython-synthio-tricks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/todbot","download_url":"https://codeload.github.com/todbot/circuitpython-synthio-tricks/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/todbot%2Fcircuitpython-synthio-tricks/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273973969,"owners_count":25200578,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-06T02:00:13.247Z","response_time":2576,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["circuitpython","i2s","raspberrypipico","rp2040","synth","synthdiy","synthesizer","synthio"],"created_at":"2024-08-09T05:00:39.331Z","updated_at":"2025-09-06T22:38:05.446Z","avatar_url":"https://github.com/todbot.png","language":null,"readme":"\n\nCircuitPython Synthio Tricks\n===============\n\nThis is a small list of tricks and techniques I use for my experiments \nin making synthesizers with [`synthio`](https://docs.circuitpython.org/en/latest/shared-bindings/synthio/index.html), and similar to my [\"circuitpython-tricks\"](https://github.com/todbot/circuitpython-tricks) page. \nSome of the synths/boards I've made that can use these techniques:\n[`pico_touch_synth`](https://github.com/todbot/picotouch_synth), \n[`qtpy_synth`](https://github.com/todbot/qtpy_synth), \n[`pico_test_synth`](https://github.com/todbot/pico_test_synth), \n[`macropadsynthplug`](https://github.com/todbot/macropadsynthplug). \nAlso check out the \"larger-tricks\" directory for other examples. \n\n\u003c!--ts--\u003e\n   * [What is synthio?](#what-is-synthio)\n      * [How synthio differs from other synthesis systems](#how-synthio-differs-from-other-synthesis-systems)\n      * [What synthio is not](#what-synthio-is-not)\n      * [Some examples](#some-examples)\n   * [Getting started](#getting-started)\n      * [Which boards does synthio work on?](#which-boards-does-synthio-work-on)\n      * [Audio out hardware](#audio-out-hardware)\n         * [Ready-made boards](#ready-made-boards)\n         * [RC filter and audiopwmio.PWMAudioOut](#rc-filter-and-audiopwmiopwmaudioout)\n         * [I2S stereo DAC](#i2s-stereo-dac)\n      * [Play a note every second](#play-a-note-every-second)\n      * [Play a chord](#play-a-chord)\n      * [USB MIDI Input](#usb-midi-input)\n      * [Serial MIDI Input](#serial-midi-input)\n      * [Using AudioMixer for adjustable volume \u0026amp; fewer glitches](#using-audiomixer-for-adjustable-volume--fewer-glitches)\n   * [Basic Synth Techniques](#basic-synth-techniques)\n      * [Amplitude envelopes](#amplitude-envelopes)\n         * [Envelope for entire synth](#envelope-for-entire-synth)\n         * [Using synthio.Note for per-note velocity envelopes](#using-synthionote-for-per-note-velocity-envelopes)\n      * [LFOs](#lfos)\n         * [Printing LFO output](#printing-lfo-output)\n         * [Vibrato: pitch bend with LFO](#vibrato-pitch-bend-with-lfo)\n         * [Tremolo: volume change with LFO](#tremolo-volume-change-with-lfo)\n      * [Pitch Bend / Portamento](#pitch-bend-portamento)\n         * [Pitch bend, by hand](#pitch-bend-by-hand)\n         * [Pitch bend, bend lfo](#pitch-bend-bend-lfo)\n      * [Waveforms](#waveforms)\n         * [Making your own waves](#making-your-own-waves)\n         * [Wavetable morphing](#wavetable-morphing)\n      * [Filters](#filters)\n      * [Filter modulation](#filter-modulation)\n   * [Advanced Techniques](#advanced-techniques)\n      * [Keeping track of pressed notes](#keeping-track-of-pressed-notes)\n      * [Detuning oscillators for fatter sound](#detuning-oscillators-for-fatter-sound)\n      * [Turn WAV files info oscillators](#turn-wav-files-info-oscillators)\n      * [Using WAV wavetables](#using-wav-wavetables)\n      * [Using LFO values in your own code](#using-lfo-values-in-your-own-code)\n      * [Using synthio.Math with synthio.LFO](#using-synthiomath-with-synthiolfo)\n      * [Drum synthesis](#drum-synthesis)\n   * [Examples](#examples)\n\n\u003c!-- Added by: tod, at: Thu Jun  1 10:59:15 PDT 2023 --\u003e\n\n\u003c!--te--\u003e\n\n## What is `synthio`?\n\n- CircuitPython [core library](https://docs.circuitpython.org/en/latest/shared-bindings/synthio/index.html)\n   available since 8.2.0-beta0 and still in development!\n- Features:\n  - Polyphonic (12 oscillator) \u0026 stereo, 16-bit, with adjustable sample rate\n  - Oscillators are single-cycle waveform-based allowing for real-time adjustable wavetables\n  - ADSR [amplitude envelope](https://docs.circuitpython.org/en/latest/shared-bindings/synthio/index.html#synthio.Envelope) per oscillator\n  - Oscillator [ring modulation](https://docs.circuitpython.org/en/latest/shared-bindings/synthio/index.html#synthio.Note.ring_frequency) w/ customizable ring oscillator waveform\n  - Extensive [LFO system](https://docs.circuitpython.org/en/latest/shared-bindings/synthio/index.html#synthio.LFO)\n    - multiple LFOs per oscillator (amplitude, panning, pitch bend, ring mod)\n    - LFOs can repeat or run once (becoming a kind of envelope)\n    - Each LFO can have a custom waveform with linear interpolation\n    - LFO outputs can be used by user code\n    - LFOs can plug into one another\n    - Customizable LFO wavetables and can be applied to your own code\n  - [Math blocks](https://docs.circuitpython.org/en/latest/shared-bindings/synthio/index.html#synthio.Math)\n     with [14 three-term Math operations](https://docs.circuitpython.org/en/latest/shared-bindings/synthio/index.html#synthio.Math) to adjust LFO ranges, offsets, scales\n  - Utility functions to easily convert from [MIDI note to frequency](https://docs.circuitpython.org/en/latest/shared-bindings/synthio/index.html#synthio.midi_to_hz) or [V/Oct modular to frequency](https://docs.circuitpython.org/en/latest/shared-bindings/synthio/index.html#synthio.voct_to_hz)\n  - Two-pole resonant low-pass (LPF) / high-pass (HPF) / band-pass (BPF) / notch filter, per-oscillator\n  - Plugs into existing the [`AudioMixer`](https://docs.circuitpython.org/en/latest/shared-bindings/audiomixer/index.html) system for use alongside `audiocore.WaveFile` sample playing\n\n### How `synthio` differs from other synthesis systems\n\nSignal flow in traditional sythesis systems is \"wired up\" once\n(either physically with circuits or virtually with software components)\nand then controlled with various inputs.   For instance, one may create oscillator, filter, and\namplifier objects, flowing audio from one to the other.\nYou then twiddle these objects to, for example, adjust pitch and trigger filter and\namplifier envelope generators.\n\nIn `synthio`, the signal chain is re-created each time a note is triggered.\nThe `synthio.Note` object is the holder of the oscillator (`note.waveform`),\nthe filter (`note.filter`), the amplitude envelope (`note.envelope`), among others.\n\nIn many cases, to change these features, you create new versions of them with different parameters,\ne.g.\n- `note.filter = synth.low_pass_filter(1200,1.3)` -- create a new LPF at 1200 Hz w/ 1.3 resonance\n- `note.envelope = synthio.Envelope(release_time=0.8)` -- create an envelope w/ 0.8 sec release time\n\nThus, if you're getting started in the reference docs, the best place to start is\n[synthio.Note](https://docs.circuitpython.org/en/latest/shared-bindings/synthio/index.html#synthio.Note).\n\n### What `synthio` is not\n\nWhile `synthio` has extensive modulation capabilities, the signal flow is fixed. It is not a modular-style\nsynthesis engine. Conceptually it is VCO-\u003eVCF-\u003eVCA and that cannot be changed.\nYou cannot treat an oscillator as an LFO, nor can you use an LFO as an audio oscillator.\n(however there is built-in ring modulation for multi-waveform mixing)\nYou cannot swap out the default 2-pole Biquad filter for a 4-pole Moog-style ladder filter emulation,\nand you cannot stack filters.\nBut since each `synthio.Note` is its own entire signal chain, you can create interesting effects by creating\nmultiple Notes at the same frequency but with different waveform, filter, amplitude, and modulation settings.\n\n\n### Some examples\n\nIf you're familiar with CircuitPython and synthesis, and want to dive in, there are larger\n[synthio-tricks examples](examples/) with wiring diagrams. In there you'll find:\n\n- [eighties_dystopia](examples/eighties_dystopia/code.py) - A swirling ominous wub that evolves over time\n- [eighties_arp](examples/eighties_arp/code.py) - An arpeggio explorer for non-musicians\n- [monosynth1](examples/monosynth1/code.py) - A complete MIDI monosynth w/ adjustable filter\n\n\n\n## Getting started\n\n### Which boards does `synthio` work on?\n\nThere's a good chance `synthio` works on your CircuitPython board. Some boards I like:\n- Adafruit QT Py RP2040 with `audiopwmio` and PWM circuit\n- Raspberry Pi Pico with `audiopwmio` and PWM circuit\n- Adafruit QT Py ESP32-S3 with `audiobusio` and PCM5102 I2S board\n- Lolin S2 Mini ESP32-S2 with `audiobusio` and PCM5102 I2S board\n\nSince `synthio` is built in to CircuitPython and CirPy has varying support on different boards,\nyou will need to check your board's \"Built-in modules avialble\" section on\n[circuitpython.org/downloads](https://circuitpython.org/downloads).\nHere's what that section looks like for the QTPy RP2040:\n\n\u003cimg src=\"./imgs/circuitpython_download_qtpyrp2040.jpg\"\u003e\n\nNote that `synthio` is there, and two audio output methods. CircuitPython supports three\ndifferent audio output techniques, with varying availability:\n\n- [`audioio.AudioOut`](https://docs.circuitpython.org/en/latest/shared-bindings/audioio/index.html)\n   -- output to built-in DAC (usually SAMD51 \"M4\" boards)\n- [`audiobusio.I2SOut`](https://docs.circuitpython.org/en/latest/shared-bindings/audiobusio/index.html)\n   -- output to external I2S DAC board (RP2040, ESP32S2/3, SAMD51 \"M4\", nRF52)\n- [`audiopwmio.PWMAudioOut`](https://docs.circuitpython.org/en/latest/shared-bindings/audiopwmio/index.html)\n   -- output PWM that needs external RC filter to convert to audio (RP2040, nRF52)\n\nNotice that not all audio output techniques are supported everywhere.\nAn I2S DAC board is the most widely supported, and highest quality.\nEven so, this guide will focus mostly on PWMAudioOut on Pico RP2040 because it's quick and simple,\nbut any of the above will work.\n\n###  Audio out hardware\n\nBecause there are many audio output methods, there are many different circuits.\n\n#### Ready-made boards\n\nThe simplest will be ready-made boards, like\n  - [PicoADK](https://github.com/DatanoiseTV/PicoADK-Hardware)\n  - [Pimoroni Pico Audio Pack](https://shop.pimoroni.com/products/pico-audio-pack)\n  - [Pimoroni Pico DV Demo Base](https://shop.pimoroni.com/products/pimoroni-pico-dv-demo-base)\n  - [Adafruit Feather RP2040 Prop-Maker](https://www.adafruit.com/product/5768)\n\nThese all have built in I2S DACs and use `audiobusio.I2SOut`.\n\n#### RC filter and `audiopwmio.PWMAudioOut`\n\n  The Pico and some other chips can output sound using PWM (~10-bit resolution) with an RC-filter.\n  (R1=1k, C1=100nF, [Sparkfun TRRS](https://www.sparkfun.com/products/11570))\n\n  \u003cimg src=\"./imgs/synthio_pico_pwm_bb.jpg\" width=500\u003e\n\n  Note: this is a very minimal RC filter stage that doesn't do DC-blocking\n  and proper line driving, but is strong enough to power many headphones.\n  See [here for a more complete RC filter circuit](https://www.youtube.com/watch?v=rwPTpMuvSXg).\n\n\n#### I2S stereo DAC\n\n  An example I2S DAC is the [I2S PCM5102](https://amzn.to/3MGOTJH).\n\n  An I2S DAC board is capable of stereo CD-quality sound and they're very affordable.\n  The line out is also strong enough to drive many headphones too, but I usually feed\n  the output into a portable bluetooth speaker with line in.\n\n  \u003cimg src=\"./imgs/synthio_pico_i2s_bb.jpg\" width=500\u003e\n\n  Note that in addition to the three I2S signals:\n   - PCM5102 BCK pin = `bit_clock`,\n   - PCM5102 LRCK pin = `word_select`\n   - PCM5102 DIN pin = `data`\n\n  you will need to wire:\n   - PCM5102 SCK pin to GND\n\n  in addition to wiring up Vin \u0026 Gnd.  For more details,  check out\n  [this post on PCM5102 modules](https://todbot.com/blog/2023/05/16/cheap-stereo-line-out-i2s-dac-for-circuitpython-arduino-synths/).\n\n\n### Play a note every second\n\nUse one of the above circuits, we can now hear what `synthio` is doing.\n\n```py\nimport board, time\nimport synthio\n\n# for PWM audio with an RC filter\nimport audiopwmio\naudio = audiopwmio.PWMAudioOut(board.GP10)\n\n# for I2S audio with external I2S DAC board\n#import audiobusio\n#audio = audiobusio.I2SOut(bit_clock=board.GP11, word_select=board.GP12, data=board.GP10)\n\n# for I2S audio on Feather RP2040 Prop-Maker\n#extpwr_pin = digitalio.DigitalInOut(board.EXTERNAL_POWER)\n#extpwr_pin.switch_to_output(value=True)\n#audio = audiobusio.I2SOut(bit_clock=board.I2S_BIT_CLOCK, word_select=board.I2S_WORD_SELECT, data=board.I2S_DATA)\n\nsynth = synthio.Synthesizer(sample_rate=22050)\naudio.play(synth)\n\nwhile True:\n    synth.press(65) # midi note 65 = F4\n    time.sleep(0.5)\n    synth.release(65) # release the note we pressed\n    time.sleep(0.5)\n```\n\nWe'll be assuming PWMAudioOut in the examples below, but if you're using an I2S DAC instead,\nthe `audio` line would look like the commented out part above. The particular choices for the three\nsignals depends on the chip, and CircuitPython will tell you in the REPL is a particular pin combination\nisn't supported. On RP2040-based boards like the Pico,\n[many pin combos are available for I2S](https://learn.adafruit.com/adafruit-i2s-stereo-decoder-uda1334a/circuitpython-wiring-test#wheres-my-i2s-2995476).\n\nThe `synthio.Synthesizer` also needs a `sample_rate` to operate at. While it can operate at 44.1 kHz CD quality,\nthese demos we will operate at half that. This will give these results a more \"low-fi\" quality but does\nfree up the Pico to do other things like update a display if you use these tricks in your own code.\n\n### Play a chord\n\nTo play notes simultaneously, send a list of notes to `synth.press()`.\nHere we send a 3-note list of [MIDI note numbers](http://notebook.zoeblade.com/Pitches.html)\nthat represent musical notes (F4, A4, C5), an F-major chord.\n\n```py\nimport board, time\nimport audiopwmio\nimport synthio\n\naudio = audiopwmio.PWMAudioOut(board.GP10)\nsynth = synthio.Synthesizer(sample_rate=22050)\naudio.play(synth)\n\nwhile True:\n  synth.press( (65,69,72) ) # midi notes 65,69,72  = F4, A4, C5\n  time.sleep(0.5)\n  synth.release( (65,69,72) )\n  time.sleep(0.5)\n```\n\n\n### USB MIDI Input\n\nHow about a MIDI synth in 20 lines of CircuitPython?\n\n(To use with a USB MIDI keyboard, plug both the keyboard \u0026 CirPy device into a computer,\nand on the computer run a DAW like Ardour, LMMS, Ableton Live, etc,\nto forward MIDI from keyboard to CirPy)\n\n```py\nimport board\nimport audiopwmio\nimport synthio\nimport usb_midi\nimport adafruit_midi\nfrom adafruit_midi.note_on import NoteOn\nfrom adafruit_midi.note_off import NoteOff\n\naudio = audiopwmio.PWMAudioOut(board.GP10)\nsynth = synthio.Synthesizer(sample_rate=22050)\naudio.play(synth)\n\nmidi = adafruit_midi.MIDI(midi_in=usb_midi.ports[0], in_channel=0 )\n\nwhile True:\n    msg = midi.receive()\n    if isinstance(msg, NoteOn) and msg.velocity != 0:\n        print(\"noteOn: \", msg.note, \"vel:\", msg.velocity)\n        synth.press( msg.note )\n    elif isinstance(msg,NoteOff) or isinstance(msg,NoteOn) and msg.velocity==0:\n        print(\"noteOff:\", msg.note, \"vel:\", msg.velocity)\n        synth.release( msg.note )\n\n```\n\n### Serial MIDI Input\n\nThe same as above, but replace the `usb_midi` with a `busio.UART`\n\n```py\n# ... as before\nimport busio\nuart = busio.UART(tx=board.TX, rx=board.RX, baudrate=31250, timeout=0.001)\nmidi = adafruit_midi.MIDI(midi_in=uart, in_channel=0 )\nwhile True:\n  msg = midi.receive()\n  # ... as before\n```\n\nFor wiring up a serial MIDI, you should check out\n[MIDI In for 3.3V Microcontrollers](https://diyelectromusic.wordpress.com/2021/02/15/midi-in-for-3-3v-microcontrollers/) page by diyelectromusic.  You can also try out\n[this 6N138-based circuit](./imgs/monosynth1_bb.png)\nI use for my [monosynth1 demo](https://www.youtube.com/watch?v=S1-TDjxE3Qs)\n\n\n### Using AudioMixer for adjustable volume \u0026 fewer glitches\n\nStick an AudioMixer in between `audio` and `synth` and we get three benefits:\n- Volume control over the entire synth\n- Can plug other players (like `WaveFile`) to play samples simultaneously\n- An audio buffer that helps eliminate glitches from other I/O\n\n```py\nimport audiomixer\naudio = audiopwmio.PWMAudioOut(board.GP10)\nmixer = audiomixer.Mixer(sample_rate=22050, buffer_size=2048)\nsynth = synthio.Synthesizer(sample_rate=22050)\naudio.play(mixer)\nmixer.voice[0].play(synth)\nmixer.voice[0].level = 0.25  # 25% volume might be better\n```\n\nSetting the AudioMixer `buffer_size` argument is handy for reducing giltches that happen when the chip is\ndoing other work like updating a display reading I2C sensors. Increase the buffer to eliminate glitches\nbut it does increase latency.\n\n## Basic Synth Techniques\n\nThere are a handful of common techniques used to make a raw electronic waveform sound more like musical\ninstruments or sounds in the real world. Here are some of them.\n\n### Amplitude envelopes\n\nThe amplitude envelope describes how a sound's loudness changes over time.\nIn synthesizers, [ADSR envelopes](https://en.wikipedia.org/wiki/Envelope_(music))\nare used to describe that change. In `synthio`, you get the standard ADSR parameters,\nand a default fast attack, max sustain level, fast release envelope.\n\n#### Envelope for entire synth\n\nTo create your own envelope with a slower attack and release time, and apply it to every note:\n\n\n```py\nimport board, time, audiopwmio, synthio\naudio = audiopwmio.PWMAudioOut(board.GP10)\nsynth = synthio.Synthesizer(sample_rate=22050)\naudio.play(synth)\n\namp_env_slow = synthio.Envelope(attack_time=0.2, release_time=0.8, sustain_level=1.0)\namp_env_fast = synthio.Envelope(attack_time=0.01, release_time=0.2, sustain_level=0.5)\nsynth.envelope = amp_env_slow  # could also set in synth constructor\n\nwhile True:\n  synth.press(65) # midi note 65 = F4\n  time.sleep(0.5)\n  synth.release(65)\n  time.sleep(1.0)\n  synth.envelope = amp_env_fast\n  synth.press(65)\n  time.sleep(0.5)\n  synth.release(65)\n  time.sleep(1.0)\n  synth.envelope = amp_env_slow\n```\n\n#### Using `synthio.Note` for per-note velocity envelopes\n\nTo give you more control over each oscillator, `synthio.Note` lets you override\nthe default envelope and waveform of your `synth` with per-note versions.\nFor instance, you can create a new envelope based on incoming MIDI note velocity to\nmake a more expressive instrument. You will have to convert MIDI notes to frequency by hand,\nbut synthio provides a helper for that.\n\n```py\nimport board, time, audiopwmio, synthio, random\naudio = audiopwmio.PWMAudioOut(board.GP10)\nsynth = synthio.Synthesizer(sample_rate=22050)\naudio.play(synth)\n\nwhile True:\n    midi_note = 65\n    velocity = random.choice( (1, 0.1, 0.5) )  # 3 different fake velocity values 0.0-1.0\n    print(\"vel:\",velocity)\n    amp_env = synthio.Envelope( attack_time=0.1 + 0.6*(1-velocity),  # high velocity, short attack\n                                release_time=0.1 + 0.9*(1-velocity) ) # low velocity, long release\n    note = synthio.Note( synthio.midi_to_hz(midi_note), envelope=amp_env )\n    synth.press(note) # press with note object\n    time.sleep(0.5)\n    synth.release(note) # must release with same note object\n    time.sleep(2.0)\n```\n\nThe choice of how you scale velocity to attack times, sustain levels and so on,\nis dependent on your application.\n\nFor an example of how to use this with MIDI velocity,\nsee [synthio_midi_synth.py](https://gist.github.com/todbot/96a654c5fa27625147d65c45c8bfd47b)\n\n\n### LFOs\n\nLFOs (Low-Frequency Oscillators) were named back when it was very different\nto build an audio-rate oscillator vs an oscillator that changed over a few seconds.\nIn synthesis, LFOs are often used to \"automate\" the knob twiddling one would do to perform an instrument.\n`synthio.LFO` is a flexible LFO system that can perform just about any kind of\nautomated twiddling you can imagine.\n\n#### Printing LFO output\n\nThe `synthio.LFO`s are also just a source of varying numbers and those numbers you can use\nas inputs to some parameter you want to vary. So you can just print them out!\nHere's the simplest way to use a `synthio.LFO`.\n\n```py\nimport time, board, synthio, audiopwmio\n\naudio = audiopwmio.PWMAudioOut(board.GP10)\nsynth = synthio.Synthesizer(sample_rate=22050)\naudio.play(synth)\n\nmylfo = synthio.LFO(rate=0.3, scale=1, offset=1)\nsynth.blocks.append(mylfo)\n\nwhile True:\n    print(mylfo.value)\n    time.sleep(0.05)\n```\n\n(instead of hooking up the LFO to a `synthio.Note` object, we're having it run globally via the\n[`synth.blocks`](https://docs.circuitpython.org/en/latest/shared-bindings/synthio/index.html#synthio.Synthesizer.blocks) feature)\n\nBy default the waveform is a triangle and you can see the output of `mylfo.value`\nsmoothly vary from 0 to 1 to 0 to -1 to 0, and so on.\nThis means it has a range of 2. If you want just a positive triangle LFO going from 0 to 1 to 0,\nyou should set `scale=0.5, offset=0.5`.\n\n\nThe waveforms for `synthio.LFO` can be any waveform, even the same waveforms used for oscillators,\nbut you can also use much smaller datasets to LFO because by default it will do interpolation\nbetween values for you.\n\nTo show the flexibilty of LFOs, here's a quick non-sound exmaple that prints out three different LFOs,\nwith custom waveforms.\n\n```py\n# orig from @jepler 15 May 2023 11:23a in #circuitpython-dev/synthio\nimport board, time, audiopwmio, synthio\nimport ulab.numpy as np\n\nSAMPLE_SIZE = 1024\nSAMPLE_VOLUME = 32767\nramp = np.linspace(-SAMPLE_VOLUME, SAMPLE_VOLUME, SAMPLE_SIZE, endpoint=False, dtype=np.int16)\nsine = np.array(\n    np.sin(np.linspace(0, 2 * np.pi, SAMPLE_SIZE, endpoint=False)) * SAMPLE_VOLUME,\n    dtype=np.int16,\n)\n\nl = synthio.LFO(ramp, rate=4, offset=1)\nm = synthio.LFO(sine, rate=2, offset=l, scale=8)\nn = synthio.LFO(sine, rate=m, offset=-2, scale=l)\nlfos = [l, m, n]\n\naudio = audiopwmio.PWMAudioOut(board.GP10)\nsynth = synthio.Synthesizer(sample_rate=22050)  # not outputting sound, just its LFO ticking\naudio.play(synth)\nsynth.blocks[:] = lfos  # attach LFOs to synth so they get ticked\n\nwhile True:\n    print(\"(\", \",\".join(str(lfo.value) for lfo in lfos), \")\" )\n    time.sleep(0.01)\n```\n\nIf you run this with the [Mu plotter](https://codewith.mu/en/tutorials/1.2/plotter)\nyou'll see all three LFOs, and you can see how the \"n\" LFO's rate is being changed by the \"m\" LFO.\n\n\u003cimg src=\"./imgs/synthio_lfo_demo1.png\" width=500\u003e\n\n\n#### Vibrato: pitch bend with LFO\n\nSome instruments like voice and violin can vary their pitch while sounding a note.\nTo emulate that, we can use an LFO.  Here we create an LFO with a rate of 5 Hz and amplitude of 0.5% max.\nFor each note, we apply that LFO to the note's `bend` property to create vibrato.\n\nIf you'd like the LFO to start over on each note on, do `lfo.retrigger()`.\n\n```py\nimport board, time, audiopwmio, synthio\naudio = audiopwmio.PWMAudioOut(board.GP10)\nsynth = synthio.Synthesizer(sample_rate=22050)\naudio.play(synth)\n\nlfo = synthio.LFO(rate=5, scale=0.05)  # 5 Hz lfo at 0.5%\n\nwhile True:\n    midi_note = 65\n    note = synthio.Note( synthio.midi_to_hz(midi_note), bend=lfo )\n    synth.press(note)\n    time.sleep(1.0)\n    synth.release(note)\n    time.sleep(1.0)\n\n```\n\n\n#### Tremolo: volume change with LFO\n\nSimilarly, we can create rhythmic changes in loudness with an LFO attached to `note.amplitude`.\nAnd since each note can get their own LFO, you can make little \"songs\" with just a few notes.\nHere's a [demo video of this \"LFO song\"](https://www.youtube.com/watch?v=m_ALNCWXor0).\n\n\n```py\nimport board, time, audiopwmio, synthio\naudio = audiopwmio.PWMAudioOut(board.GP10)\nsynth = synthio.Synthesizer(sample_rate=22050)\naudio.play(synth)\n\nlfo_tremo1 = synthio.LFO(rate=3)  # 3 Hz for fastest note\nlfo_tremo2 = synthio.LFO(rate=2)  # 2 Hz for middle note\nlfo_tremo3 = synthio.LFO(rate=1)  # 1 Hz for lower note\nlfo_tremo4 = synthio.LFO(rate=0.75) # 0.75 Hz for lowest bass note\n\nmidi_note = 65\nnote1 = synthio.Note( synthio.midi_to_hz(midi_note), amplitude=lfo_tremo1)\nnote2 = synthio.Note( synthio.midi_to_hz(midi_note-7), amplitude=lfo_tremo2)\nnote3 = synthio.Note( synthio.midi_to_hz(midi_note-12), amplitude=lfo_tremo3)\nnote4 = synthio.Note( synthio.midi_to_hz(midi_note-24), amplitude=lfo_tremo4)\nsynth.press( (note1, note2, note3, note4) )\n\nwhile True:\n    print(\"hi, we're just groovin\")\n    time.sleep(1)\n```\n\n         \n### Pitch bend / Portamento\n\nPitch bend, portamento, pitch glide, or glissando are all roughly equivalent in \nsynthesizers: a continuous smooth glide between two notes. While `synthio` doesn't\nprovide this exact functionality, we can achieve the effect via a variety of means. \n\n#### Pitch bend, by hand\n\nThere are several different ways to glide the pitch from one note to another. \nThe most obvious way is to do it \"by hand\" by modifying the `note.frequency` property \nover time. (orig from a [discussion w/@shion on mastodon](https://mastodon.social/@todbot/112610331112413354))\n\n```py\n# ... synthio audio set up as normal ...\ndef bend_note(note, start_notenum, end_notenum, bend_time=3):\n    bend_steps = 100  # arbitrarily chosen\n    bend_deltat = bend_time / bend_steps\n    f = synthio.midi_to_hz(start_notenum)\n    for i in range(glide_steps):\n        slid_notenum = start_notenum + i*((end_notenum - start_notenum)/bend_steps)\n        note.frequency = synthio.midi_to_hz(slid_notenum)\n        time.sleep(bend_deltat)  # note the time.sleep()!\n\nwhile True:\n    note = synthio.Note(synthio.midi_to_hz(70))\n    synth.press(note)\n    note_glide(note, 70,30)\n    note_glide(note, 30,40, 0.1)\n    note_glide(note, 40,70, 0.1)\n    synth.release(note)\n```\n\n#### Pitch bend, bend lfo\n\nThe above approach isn't very efficient. So far the best way I've found to do \npitch-bend is to use an LFO on the `note.bend` property, like with [vibrato](#vibrato-pitch-bend-with-lfo),\nbut with a specially-constructed \"line\" LFO in one-shot mode. \nFor a demo of the below code, [see this post](https://mastodon.social/@todbot/112792760148292105).\n\n```py\n# ... synthio audio set up as normal ...\ndef bend_note(note, start_notenum, end_notenum, bend_time=1):\n    note.frequency = synthio.midi_to_hz(start_notenum)\n    bend_amount = (end_notenum - start_notenum) / 12\n    # special two-point line LFO that goes from 0 to bend_amount\n    bend_lfo = synthio.LFO( waveform=np.linspace(-16384, 16383, num=2, dtype=np.int16),\n        rate=1/bend_time, scale=bend_amount, offset=bend_amount/2, once=True)\n    note.bend = bend_lfo\n\nstart_notenum = 40  # E2\nend_notenum = 52  # E3\nwhile True:\n    print(\"start:\", start_notenum, \"end:\", end_notenum)\n    note = synthio.Note(synthio.midi_to_hz(start_notenum), panning=0 )\n    synth.press(note)\n    time.sleep(2)\n    bend_note(note, start_notenum, end_notenum, 0.75)\n    synth.release(note)\n    time.sleep(1)\n    start_notenum = end_notenum  # save end note so we can pick new end note\n    end_notenum = random.randint(22,64)\n```\nNote that in addition to passing in the start note number to `synthio.Note()`, \nwe must pass in the start note number and end MIDI note number to `bend_note()`.\n\n\n### Waveforms\n\nThe default oscillator waveform in `synthio.Synthesizer` is a square-wave with 50% duty-cycle.\nBut synthio will accept any buffer of data and treat it as a single-cycle waveform.\nOne of the easiest ways to make the waveform buffers that synthio expects is to use\n[`ulab.numpy`](https://learn.adafruit.com/ulab-crunch-numbers-fast-with-circuitpython/ulab-numpy-phrasebook).\nThe numpy functions also have useful tools like `np.linspace()` to generate a line through a number space\nand trig functions like `np.sin()`. Once you have a waveform, set it with either `synth.waveform`\nor creating a new `synthio.Note(waveform=...)`\n\n#### Making your own waves\n\nHere's an example that creates two new waveforms: a sine way and a sawtooth wave, and then plays\nthem a two-note chord, first with sine waves, then with sawtooth waves.\n\n```py\nimport board, time, audiopwmio, synthio\nimport ulab.numpy as np\naudio = audiopwmio.PWMAudioOut(board.GP10)\nsynth = synthio.Synthesizer(sample_rate=22050)\naudio.play(synth)\n# create sine \u0026 sawtooth single-cycle waveforms to act as oscillators\nSAMPLE_SIZE = 512\nSAMPLE_VOLUME = 32000  # 0-32767\nwave_sine = np.array(np.sin(np.linspace(0, 2*np.pi, SAMPLE_SIZE, endpoint=False)) * SAMPLE_VOLUME, dtype=np.int16)\nwave_saw = np.linspace(SAMPLE_VOLUME, -SAMPLE_VOLUME, num=SAMPLE_SIZE, dtype=np.int16)\n\nmidi_note = 65\nmy_wave = wave_saw\nwhile True:\n    # create notes using those waveforms\n    note1 = synthio.Note( synthio.midi_to_hz(midi_note), waveform=my_wave)\n    note2 = synthio.Note( synthio.midi_to_hz(midi_note-7), waveform=my_wave)\n    synth.press(note1)\n    time.sleep(0.5)\n    synth.press(note2)\n    time.sleep(1)\n    synth.release( (note1,note2) )\n    time.sleep(0.1)\n    my_wave = wave_sine if my_wave is wave_saw else wave_saw  # toggle waveform\n```\n\n#### Wavetable morphing\n\nOne of the coolest things about `synthio` being wavetable-based, is that we can alter the `waveform`\n_in real time_!\n\nGiven the above setup but replacing the \"while\" loop, this will mix between the sine \u0026 square wave.\n\nThe trick here is that we give the `synthio.Note` object an initial empty waveform buffer\nand then instead of replacing that buffer with `note.waveform = some_wave` we copy with `note.waveform[:] = some_wave`.\n\n(This avoids needing an additional `np.int16` result buffer for `lerp()`, since lerp-ing results in a `np.float32` array)\n\n```py\n[... hardware setup from above ...]\n# create sine \u0026 sawtooth single-cycle waveforms to act as oscillators\nSAMPLE_SIZE = 512\nSAMPLE_VOLUME = 32000  # 0-32767\n\nwave_sine = np.array(np.sin(np.linspace(0, 2*np.pi, SAMPLE_SIZE, endpoint=False)) * SAMPLE_VOLUME, dtype=np.int16)\nwave_saw = np.linspace(SAMPLE_VOLUME, -SAMPLE_VOLUME, num=SAMPLE_SIZE, dtype=np.int16)\n\n# mix between values a and b, works with numpy arrays too,  t ranges 0-1\ndef lerp(a, b, t):  return (1-t)*a + t*b\n\nwave_empty = np.zeros(SAMPLE_SIZE, dtype=np.int16)  # empty buffer we'll use array slice copy \"[:]\" on\nnote = synthio.Note( frequency=220, waveform=wave_empty)\nsynth.press(note)\n\npos = 0\nwhile True:\n  print(pos)\n  note.waveform[:] = lerp(wave_sine, wave_saw, pos)\n  pos += 0.01\n  if pos \u003e=1: pos = 0\n  time.sleep(0.01)\n```\n\n#### Filters\n\nFilters let you change the character / timbre of the raw oscillator sound.\nThe filter algorithm in `synthio` is a Biquad filter, giving a two-pole (12dB)\nlow-pass (LP), high-pass (HP), or band-pass (BP) filters.\n\nTo set a filter at a fixed frequency, set the `Note.filter` property\nusing one of the `synthio.*_filter()` methods:\n\n```py\n[ ... synthio setup as normal ...]\n\nfrequency = 2000\nresonance = 1.5\n\nlpf = synth.low_pass_filter(frequency,resonance)\nhpf = synth.high_pass_filter(frequency,resonance)\nbpf = synth.band_pass_filter(frequency,resonance)\n\nnote1 = synth.Note(frequency=220, filter=lpf)\nnote2 = synth.Note(frequency=330, filter=hpf)\nnote3 = synth.Note(frequency=440, filter=bpf)\n```\n\nNote that making a filter is a complex operation, requiring a function,\nand you cannot set the properties of a resulting filter after its created.\nThis makes modulating the filter a bit trickier.\n\nAlso note that currently there are some glitchy instabilties in the filter\nwhen resonance is 2 or greater and filter frequency is close to note frequency.\n\n#### Filter modulation\n\nThe standard synthio approach to modulation is to create a `synthio.LFO` and attach it to a property.\n(see above LFO examples)  The properties must be of type `synthio.BlockInput` for this to work, though.\nNot all synthio properties are `BlockInputs`, most notably, the `Note.filter` property.\n\nSo one way to modulate a filter is to use Python:\n\n```py\n# fake a looping ramp down filter sweep\nfmin = 100\nfmax = 1000\nf = fmax\nnote = synth.Note(frequency=220)\nsynth.play(note)\nwhile True:\n  note.filter = synth.low_pass_filter(f, 1.5)  # adjust note's filter\n  f = f - 10\n  if f \u003c fmin: f = fmax\n  time.sleep(0.01)  # sleep determines our ramp rate\n```\n\nA more \"synthio\" way to modulate filter is to use an LFO but hand-copying LFO value to the filter.\nThis requires adding the LFO to the\n[`synth.blocks`](https://docs.circuitpython.org/en/latest/shared-bindings/synthio/index.html#synthio.Synthesizer.blocks)\nglobal runner since the LFO is not directly associated with a `Note`.\n\n```py\nfmin = 100\nfmax = 1000\nramp_down = np.array( (32767,0), dtype=np.int16) # unpolar ramp down, when interpolated by LFO\n\nf_lfo = synth.LFO(rate=0.3, scale=fmax-fmin, offset=fmin, waveform=ramp_down)\nsynth.blocks.append(f_lfo)  # add lfo to global LFO runner to get it to tick\n\nnote = synth.Note(frequency=220)\nsynth.play(note)  # start note sounding\n\nwhile True:\n  note.filter = synth.low_pass_filter(f_lfo.value, 1.5) # adjust its filter\n  time.sleep(0.001)\n```\n\nThis is a fairly advanced technique as it requires keeping track of the LFO objects stuffed\ninto `synth.blocks` so they can be removed later.  See \"Keeping track of pressed notes\" below for\none technique for doing this.\n\n\n## Advanced Techniques\n\n\n### Keeping track of pressed notes\n\nWhen passing `synthio.Note` objects to `synth.press()` instead of MIDI note numbers,\nyour code must remmeber that `Note` object so it can pass it into `synth.release()` to stop it playing.\n\nOne way to do this is with a Python dict, where the key is whatever your unique identifier is\n(e.g. MIDI note number here for simplicity) and the value is the note object.\n\n```py\n\n# setup as before to get `synth` \u0026 `midi` objects\nnotes_pressed = {}  # which notes being pressed. key=midi note, val=note object\nwhile True:\n    msg = midi.receive()\n    if isinstance(msg, NoteOn) and msg.velocity != 0:  # NoteOn\n        note = synthio.Note(frequency=synthio.midi_to_hz(msg.note), waveform=wave_saw, #..etc )\n        synthio.press(note)\n        notes_pressed[msg.note] = note\n    elif isinstance(msg,NoteOff) or isinstance(msg,NoteOn) and msg.velocity==0:  # NoteOff\n        note = notes_pressed.get(msg.note, None) # let's us get back None w/o try/except\n        if note:\n            syntho.release(note)\n```\n\n\n### Detuning oscillators for fatter sound\n\nSince we have fine-grained control over a note's frequency with `note.frequency`, this means we can do a\ncommon technique for getting a \"fatter\" sound.\n\n```py\nimport board, time, audiopwmio, synthio\naudio = audiopwmio.PWMAudioOut(board.TX)\nsynth = synthio.Synthesizer(sample_rate=22050)\naudio.play(synth)\n\ndetune = 0.005  # how much to detune, 0.7% here\nnum_oscs = 1\nmidi_note = 45\nwhile True:\n    print(\"num_oscs:\", num_oscs)\n    notes = []  # holds note objs being pressed\n    # simple detune, always detunes up\n    for i in range(num_oscs):\n        f = synthio.midi_to_hz(midi_note) * (1 + i*detune)\n        notes.append( synthio.Note(frequency=f) )\n    synth.press(notes)\n    time.sleep(1)\n    synth.release(notes)\n    time.sleep(0.1)\n    # increment number of detuned oscillators\n    num_oscs = num_oscs+1 if num_oscs \u003c 5 else 1\n```\n\n\n### Turn WAV files info oscillators\n\nThanks to [`adafruit_wave`](https://github.com/adafruit/Adafruit_CircuitPython_wave) it is really\neasy to load up a WAV file into a buffer and use it as a synthio waveform. Two great repositories of\nsingle-cycle waveforms are [AKWF FREE](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/)\nand [waveeditonline.com](http://waveeditonline.com/)\n\n```py\n# orig from @jepler 31 May 2023 1:34p #circuitpython-dev/synthio\nimport adafruit_wave\n\n# reads in entire wave\ndef read_waveform(filename):\n    with adafruit_wave.open(filename) as w:\n        if w.getsampwidth() != 2 or w.getnchannels() != 1:\n            raise ValueError(\"unsupported format\")\n        return memoryview(w.readframes(w.getnframes())).cast('h')\n\n# this verion lets you lerp() it to mix w/ another wave\ndef read_waveform_ulab(filename):\n    with adafruit_wave.open(filename) as w:\n        if w.getsampwidth() != 2 or w.getnchannels() != 1:\n            raise ValueError(\"unsupported format\")\n        return np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16)\n\n\nmy_wave = read_waveform(\"AKWF_granular_0001.wav\")\n\n```\n\n### Using WAV Wavetables\n\nThe [waveeditonline.com](http://waveeditonline.com/) site has specially constructed WAV files\ncalled \"wavetables\" that contain 64 single-cycle waveforms, each waveform having 256 samples.\nThe waveforms in a wavetable are usually harmonically-related, so scanning through them\ncan produce interesting effects that could sound similar to using a filter,\nwithout needing to use `synth.filter`!\n\nThe code below will load up one of these wavetables, and let you pick different\nwaveforms within by setting `wavetable.set_wave_pos(n)`.\n\n```py\nimport board, time, audiopwmio, synthio\nimport ulab.numpy as np\nimport adafruit_wave\n\naudio = audiopwmio.PWMAudioOut(board.GP10)\nsynth = synthio.Synthesizer(sample_rate=22050)\naudio.play(synth)\n\n# mix between values a and b, works with numpy arrays too,  t ranges 0-1\ndef lerp(a, b, t):  return (1-t)*a + t*b\n\nclass Wavetable:\n    def __init__(self, filepath, wave_len=256):\n        self.w = adafruit_wave.open(filepath)\n        self.wave_len = wave_len  # how many samples in each wave\n        if self.w.getsampwidth() != 2 or self.w.getnchannels() != 1:\n            raise ValueError(\"unsupported WAV format\")\n        self.waveform = np.zeros(wave_len, dtype=np.int16)  # empty buffer we'll copy into\n        self.num_waves = self.w.getnframes() / self.wave_len\n\n    def set_wave_pos(self, pos):\n        \"\"\"Pick where in wavetable to be, morphing between waves\"\"\"\n        pos = min(max(pos, 0), self.num_waves-1)  # constrain\n        samp_pos = int(pos) * self.wave_len  # get sample position\n        self.w.setpos(samp_pos)\n        waveA = np.frombuffer(self.w.readframes(self.wave_len), dtype=np.int16)\n        self.w.setpos(samp_pos + self.wave_len)  # one wave up\n        waveB = np.frombuffer(self.w.readframes(self.wave_len), dtype=np.int16)\n        pos_frac = pos - int(pos)  # fractional position between wave A \u0026 B\n        self.waveform[:] = lerp(waveA, waveB, pos_frac) # mix waveforms A \u0026 B\n\nwavetable = Wavetable(\"BRAIDS02.WAV\", 256) # from http://waveeditonline.com/index-17.html\n\nnote = synthio.Note(frequency=220, waveform=wavetable.waveform)\nsynth.press( note )  # start an oscillator going\n\n# scan through the wavetable, morphing through each one\ni=0\ndi=0.1  # how fast to scan\nwhile True:\n    i = i + di\n    if i \u003c=0 or i \u003e= wavetable.num_waves: di = -di\n    wavetable.set_wave_pos(i)\n    time.sleep(0.001)\n```\n\n\n### Using LFO values in your own code\n\n[tbd]\n\n### Using `synthio.Math` with `synthio.LFO`\n\n[tbd]\n\n### Drum synthesis\n\n[tbd, but check out [gamblor's drums.py gist](https://gist.github.com/gamblor21/15a430929abf0e10eeaba8a45b01f5a8)]\n\n### Examples\n\nHere are some [larger synthio-tricks examples with wiring diagrams](examples/).\n\n\n### Troubleshooting\n\n\n#### Glitches when `code.py` is saved or CIRCUITPY drive access\n\n#### Distortion in audio\n\n####\n","funding_links":[],"categories":["Code"],"sub_categories":["Educational"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftodbot%2Fcircuitpython-synthio-tricks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftodbot%2Fcircuitpython-synthio-tricks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftodbot%2Fcircuitpython-synthio-tricks/lists"}