Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

tips, tricks, and examples of using CircuitPython synthio

License

NotificationsYou must be signed in to change notification settings

todbot/circuitpython-synthio-tricks

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

81 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

This is a small list of tricks and techniques I use for my experimentsin making synthesizers withsynthio, and similar to my"circuitpython-tricks" page.Some of the synths/boards I've made that can use these techniques:pico_touch_synth,qtpy_synth,pico_test_synth,macropadsynthplug.Also check out the "larger-tricks" directory for other examples.

And for a more lesson-based approach, see theCircuitPython_Synthio_Tutorial.

What issynthio?

  • CircuitPythoncore libraryavailable since 8.2.0-beta0 and still in development!
  • Features:
    • Polyphonic (12 oscillator) & stereo, 16-bit, with adjustable sample rate
    • Oscillators are single-cycle waveform-based allowing for real-time adjustable wavetables
    • ADSRamplitude envelope per oscillator
    • Oscillatorring modulation w/ customizable ring oscillator waveform
    • ExtensiveLFO system
      • multiple LFOs per oscillator (amplitude, panning, pitch bend, ring mod)
      • LFOs can repeat or run once (becoming a kind of envelope)
      • Each LFO can have a custom waveform with linear interpolation
      • LFO outputs can be used by user code
      • LFOs can plug into one another
      • Customizable LFO wavetables and can be applied to your own code
    • Math blockswith14 three-term Math operations to adjust LFO ranges, offsets, scales
    • Utility functions to easily convert fromMIDI note to frequency orV/Oct modular to frequency
    • Two-pole resonant low-pass (LPF) / high-pass (HPF) / band-pass (BPF) / notch filter, per-oscillator
    • Plugs into existing theAudioMixer system for use alongsideaudiocore.WaveFile sample playing

Howsynthio differs from other synthesis systems

Signal flow in traditional sythesis systems is "wired up" once(either physically with circuits or virtually with software components)and then controlled with various inputs. For instance, one may create oscillator, filter, andamplifier objects, flowing audio from one to the other.You then twiddle these objects to, for example, adjust pitch and trigger filter andamplifier envelope generators.

Insynthio, the signal chain is re-created each time a note is triggered.Thesynthio.Note object is the holder of the oscillator (note.waveform),the filter (note.filter), the amplitude envelope (note.envelope), among others.

In many cases, to change these features, you create new versions of them with different parameters,e.g.

  • note.filter = synth.low_pass_filter(1200,1.3) -- create a new LPF at 1200 Hz w/ 1.3 resonance
  • note.envelope = synthio.Envelope(release_time=0.8) -- create an envelope w/ 0.8 sec release time

Thus, if you're getting started in the reference docs, the best place to start issynthio.Note.

Whatsynthio is not

Whilesynthio has extensive modulation capabilities, the signal flow is fixed. It is not a modular-stylesynthesis engine. Conceptually it is VCO->VCF->VCA and that cannot be changed.You cannot treat an oscillator as an LFO, nor can you use an LFO as an audio oscillator.(however there is built-in ring modulation for multi-waveform mixing)You cannot swap out the default 2-pole Biquad filter for a 4-pole Moog-style ladder filter emulation,and you cannot stack filters.But since eachsynthio.Note is its own entire signal chain, you can create interesting effects by creatingmultiple Notes at the same frequency but with different waveform, filter, amplitude, and modulation settings.

Some examples

If you're familiar with CircuitPython and synthesis, and want to dive in, there are largersynthio-tricks examples with wiring diagrams. In there you'll find:

Getting started

Which boards doessynthio work on?

There's a good chancesynthio works on your CircuitPython board. Some boards I like:

  • Adafruit QT Py RP2040 withaudiopwmio and PWM circuit
  • Raspberry Pi Pico withaudiopwmio and PWM circuit
  • Adafruit QT Py ESP32-S3 withaudiobusio and PCM5102 I2S board
  • Lolin S2 Mini ESP32-S2 withaudiobusio and PCM5102 I2S board

Sincesynthio is built in to CircuitPython and CirPy has varying support on different boards,you will need to check your board's "Built-in modules avialble" section oncircuitpython.org/downloads.Here's what that section looks like for the QTPy RP2040:

Note thatsynthio is there, and two audio output methods. CircuitPython supports threedifferent audio output techniques, with varying availability:

Notice that not all audio output techniques are supported everywhere.An I2S DAC board is the most widely supported, and highest quality.Even so, this guide will focus mostly on PWMAudioOut on Pico RP2040 because it's quick and simple,but any of the above will work.

Audio out hardware

Because there are many audio output methods, there are many different circuits.

Ready-made boards

The simplest will be ready-made boards, like

These all have built in I2S DACs and useaudiobusio.I2SOut.

RC filter andaudiopwmio.PWMAudioOut

The Pico and some other chips can output sound using PWM (~10-bit resolution) with an RC-filter.(R1=1k, C1=100nF,Sparkfun TRRS)

Note: this is a very minimal RC filter stage that doesn't do DC-blockingand proper line driving, but is strong enough to power many headphones.Seehere for a more complete RC filter circuit.

I2S stereo DAC

An example I2S DAC is theI2S PCM5102.

An I2S DAC board is capable of stereo CD-quality sound and they're very affordable.The line out is also strong enough to drive many headphones too, but I usually feedthe output into a portable bluetooth speaker with line in.

Note that in addition to the three I2S signals:

  • PCM5102 BCK pin =bit_clock,
  • PCM5102 LRCK pin =word_select
  • PCM5102 DIN pin =data

you will need to wire:

  • PCM5102 SCK pin to GND

in addition to wiring up Vin & Gnd. For more details, check outthis post on PCM5102 modules.

Play a note every second

Use one of the above circuits, we can now hear whatsynthio is doing.

importboard,timeimportsynthio# for PWM audio with an RC filterimportaudiopwmioaudio=audiopwmio.PWMAudioOut(board.GP10)# for I2S audio with external I2S DAC board#import audiobusio#audio = audiobusio.I2SOut(bit_clock=board.GP11, word_select=board.GP12, data=board.GP10)# for I2S audio on Feather RP2040 Prop-Maker#extpwr_pin = digitalio.DigitalInOut(board.EXTERNAL_POWER)#extpwr_pin.switch_to_output(value=True)#audio = audiobusio.I2SOut(bit_clock=board.I2S_BIT_CLOCK, word_select=board.I2S_WORD_SELECT, data=board.I2S_DATA)synth=synthio.Synthesizer(sample_rate=22050)audio.play(synth)whileTrue:synth.press(65)# midi note 65 = F4time.sleep(0.5)synth.release(65)# release the note we pressedtime.sleep(0.5)

We'll be assuming PWMAudioOut in the examples below, but if you're using an I2S DAC instead,theaudio line would look like the commented out part above. The particular choices for the threesignals depends on the chip, and CircuitPython will tell you in the REPL is a particular pin combinationisn't supported. On RP2040-based boards like the Pico,many pin combos are available for I2S.

Thesynthio.Synthesizer also needs asample_rate to operate at. While it can operate at 44.1 kHz CD quality,these demos we will operate at half that. This will give these results a more "low-fi" quality but doesfree up the Pico to do other things like update a display if you use these tricks in your own code.

Play a chord

To play notes simultaneously, send a list of notes tosynth.press().Here we send a 3-note list ofMIDI note numbersthat represent musical notes (F4, A4, C5), an F-major chord.

importboard,timeimportaudiopwmioimportsynthioaudio=audiopwmio.PWMAudioOut(board.GP10)synth=synthio.Synthesizer(sample_rate=22050)audio.play(synth)whileTrue:synth.press( (65,69,72) )# midi notes 65,69,72  = F4, A4, C5time.sleep(0.5)synth.release( (65,69,72) )time.sleep(0.5)

USB MIDI Input

How about a MIDI synth in 20 lines of CircuitPython?

(To use with a USB MIDI keyboard, plug both the keyboard & CirPy device into a computer,and on the computer run a DAW like Ardour, LMMS, Ableton Live, etc,to forward MIDI from keyboard to CirPy)

importboardimportaudiopwmioimportsynthioimportusb_midiimportadafruit_midifromadafruit_midi.note_onimportNoteOnfromadafruit_midi.note_offimportNoteOffaudio=audiopwmio.PWMAudioOut(board.GP10)synth=synthio.Synthesizer(sample_rate=22050)audio.play(synth)midi=adafruit_midi.MIDI(midi_in=usb_midi.ports[0],in_channel=0 )whileTrue:msg=midi.receive()ifisinstance(msg,NoteOn)andmsg.velocity!=0:print("noteOn: ",msg.note,"vel:",msg.velocity)synth.press(msg.note )elifisinstance(msg,NoteOff)orisinstance(msg,NoteOn)andmsg.velocity==0:print("noteOff:",msg.note,"vel:",msg.velocity)synth.release(msg.note )

Serial MIDI Input

The same as above, but replace theusb_midi with abusio.UART

# ... as beforeimportbusiouart=busio.UART(tx=board.TX,rx=board.RX,baudrate=31250,timeout=0.001)midi=adafruit_midi.MIDI(midi_in=uart,in_channel=0 )whileTrue:msg=midi.receive()# ... as before

For wiring up a serial MIDI, you should check outMIDI In for 3.3V Microcontrollers page by diyelectromusic. You can also try outthis 6N138-based circuitI use for mymonosynth1 demo

Using AudioMixer for adjustable volume & fewer glitches

Stick an AudioMixer in betweenaudio andsynth and we get three benefits:

  • Volume control over the entire synth
  • Can plug other players (likeWaveFile) to play samples simultaneously
  • An audio buffer that helps eliminate glitches from other I/O
importaudiomixeraudio=audiopwmio.PWMAudioOut(board.GP10)mixer=audiomixer.Mixer(sample_rate=22050,buffer_size=2048)synth=synthio.Synthesizer(sample_rate=22050)audio.play(mixer)mixer.voice[0].play(synth)mixer.voice[0].level=0.25# 25% volume might be better

Setting the AudioMixerbuffer_size argument is handy for reducing giltches that happen when the chip isdoing other work like updating a display reading I2C sensors. Increase the buffer to eliminate glitchesbut it does increase latency.

Basic Synth Techniques

There are a handful of common techniques used to make a raw electronic waveform sound more like musicalinstruments or sounds in the real world. Here are some of them.

Amplitude envelopes

The amplitude envelope describes how a sound's loudness changes over time.In synthesizers,ADSR envelopesare used to describe that change. Insynthio, you get the standard ADSR parameters,and a default fast attack, max sustain level, fast release envelope.

Envelope for entire synth

To create your own envelope with a slower attack and release time, and apply it to every note:

importboard,time,audiopwmio,synthioaudio=audiopwmio.PWMAudioOut(board.GP10)synth=synthio.Synthesizer(sample_rate=22050)audio.play(synth)amp_env_slow=synthio.Envelope(attack_time=0.2,release_time=0.8,sustain_level=1.0)amp_env_fast=synthio.Envelope(attack_time=0.01,release_time=0.2,sustain_level=0.5)synth.envelope=amp_env_slow# could also set in synth constructorwhileTrue:synth.press(65)# midi note 65 = F4time.sleep(0.5)synth.release(65)time.sleep(1.0)synth.envelope=amp_env_fastsynth.press(65)time.sleep(0.5)synth.release(65)time.sleep(1.0)synth.envelope=amp_env_slow

Usingsynthio.Note for per-note velocity envelopes

To give you more control over each oscillator,synthio.Note lets you overridethe default envelope and waveform of yoursynth with per-note versions.For instance, you can create a new envelope based on incoming MIDI note velocity tomake a more expressive instrument. You will have to convert MIDI notes to frequency by hand,but synthio provides a helper for that.

importboard,time,audiopwmio,synthio,randomaudio=audiopwmio.PWMAudioOut(board.GP10)synth=synthio.Synthesizer(sample_rate=22050)audio.play(synth)whileTrue:midi_note=65velocity=random.choice( (1,0.1,0.5) )# 3 different fake velocity values 0.0-1.0print("vel:",velocity)amp_env=synthio.Envelope(attack_time=0.1+0.6*(1-velocity),# high velocity, short attackrelease_time=0.1+0.9*(1-velocity) )# low velocity, long releasenote=synthio.Note(synthio.midi_to_hz(midi_note),envelope=amp_env )synth.press(note)# press with note objecttime.sleep(0.5)synth.release(note)# must release with same note objecttime.sleep(2.0)

The choice of how you scale velocity to attack times, sustain levels and so on,is dependent on your application.

For an example of how to use this with MIDI velocity,seesynthio_midi_synth.py

LFOs

LFOs (Low-Frequency Oscillators) were named back when it was very differentto build an audio-rate oscillator vs an oscillator that changed over a few seconds.In synthesis, LFOs are often used to "automate" the knob twiddling one would do to perform an instrument.synthio.LFO is a flexible LFO system that can perform just about any kind ofautomated twiddling you can imagine.

Printing LFO output

Thesynthio.LFOs are also just a source of varying numbers and those numbers you can useas inputs to some parameter you want to vary. So you can just print them out!Here's the simplest way to use asynthio.LFO.

importtime,board,synthio,audiopwmioaudio=audiopwmio.PWMAudioOut(board.GP10)synth=synthio.Synthesizer(sample_rate=22050)audio.play(synth)mylfo=synthio.LFO(rate=0.3,scale=1,offset=1)synth.blocks.append(mylfo)whileTrue:print(mylfo.value)time.sleep(0.05)

(instead of hooking up the LFO to asynthio.Note object, we're having it run globally via thesynth.blocks feature)

By default the waveform is a triangle and you can see the output ofmylfo.valuesmoothly vary from 0 to 1 to 0 to -1 to 0, and so on.This means it has a range of 2. If you want just a positive triangle LFO going from 0 to 1 to 0,you should setscale=0.5, offset=0.5.

The waveforms forsynthio.LFO can be any waveform, even the same waveforms used for oscillators,but you can also use much smaller datasets to LFO because by default it will do interpolationbetween values for you.

To show the flexibilty of LFOs, here's a quick non-sound exmaple that prints out three different LFOs,with custom waveforms.

# orig from @jepler 15 May 2023 11:23a in #circuitpython-dev/synthioimportboard,time,audiopwmio,synthioimportulab.numpyasnpSAMPLE_SIZE=1024SAMPLE_VOLUME=32767ramp=np.linspace(-SAMPLE_VOLUME,SAMPLE_VOLUME,SAMPLE_SIZE,endpoint=False,dtype=np.int16)sine=np.array(np.sin(np.linspace(0,2*np.pi,SAMPLE_SIZE,endpoint=False))*SAMPLE_VOLUME,dtype=np.int16,)l=synthio.LFO(ramp,rate=4,offset=1)m=synthio.LFO(sine,rate=2,offset=l,scale=8)n=synthio.LFO(sine,rate=m,offset=-2,scale=l)lfos= [l,m,n]audio=audiopwmio.PWMAudioOut(board.GP10)synth=synthio.Synthesizer(sample_rate=22050)# not outputting sound, just its LFO tickingaudio.play(synth)synth.blocks[:]=lfos# attach LFOs to synth so they get tickedwhileTrue:print("(",",".join(str(lfo.value)forlfoinlfos),")" )time.sleep(0.01)

If you run this with theMu plotteryou'll see all three LFOs, and you can see how the "n" LFO's rate is being changed by the "m" LFO.

Vibrato: pitch bend with LFO

Some instruments like voice and violin can vary their pitch while sounding a note.To emulate that, we can use an LFO. Here we create an LFO with a rate of 5 Hz and amplitude of 0.5% max.For each note, we apply that LFO to the note'sbend property to create vibrato.

If you'd like the LFO to start over on each note on, dolfo.retrigger().

importboard,time,audiopwmio,synthioaudio=audiopwmio.PWMAudioOut(board.GP10)synth=synthio.Synthesizer(sample_rate=22050)audio.play(synth)lfo=synthio.LFO(rate=5,scale=0.05)# 5 Hz lfo at 0.5%whileTrue:midi_note=65note=synthio.Note(synthio.midi_to_hz(midi_note),bend=lfo )synth.press(note)time.sleep(1.0)synth.release(note)time.sleep(1.0)

Tremolo: volume change with LFO

Similarly, we can create rhythmic changes in loudness with an LFO attached tonote.amplitude.And since each note can get their own LFO, you can make little "songs" with just a few notes.Here's ademo video of this "LFO song".

importboard,time,audiopwmio,synthioaudio=audiopwmio.PWMAudioOut(board.GP10)synth=synthio.Synthesizer(sample_rate=22050)audio.play(synth)lfo_tremo1=synthio.LFO(rate=3)# 3 Hz for fastest notelfo_tremo2=synthio.LFO(rate=2)# 2 Hz for middle notelfo_tremo3=synthio.LFO(rate=1)# 1 Hz for lower notelfo_tremo4=synthio.LFO(rate=0.75)# 0.75 Hz for lowest bass notemidi_note=65note1=synthio.Note(synthio.midi_to_hz(midi_note),amplitude=lfo_tremo1)note2=synthio.Note(synthio.midi_to_hz(midi_note-7),amplitude=lfo_tremo2)note3=synthio.Note(synthio.midi_to_hz(midi_note-12),amplitude=lfo_tremo3)note4=synthio.Note(synthio.midi_to_hz(midi_note-24),amplitude=lfo_tremo4)synth.press( (note1,note2,note3,note4) )whileTrue:print("hi, we're just groovin")time.sleep(1)

Pitch bend / Portamento

Pitch bend, portamento, pitch glide, or glissando are all roughly equivalent insynthesizers: a continuous smooth glide between two notes. Whilesynthio doesn'tprovide this exact functionality, we can achieve the effect via a variety of means.

Pitch bend, by hand

There are several different ways to glide the pitch from one note to another.The most obvious way is to do it "by hand" by modifying thenote.frequency propertyover time. (orig from adiscussion w/@shion on mastodon)

# ... synthio audio set up as normal ...defbend_note(note,start_notenum,end_notenum,bend_time=3):bend_steps=100# arbitrarily chosenbend_deltat=bend_time/bend_stepsf=synthio.midi_to_hz(start_notenum)foriinrange(glide_steps):slid_notenum=start_notenum+i*((end_notenum-start_notenum)/bend_steps)note.frequency=synthio.midi_to_hz(slid_notenum)time.sleep(bend_deltat)# note the time.sleep()!whileTrue:note=synthio.Note(synthio.midi_to_hz(70))synth.press(note)note_glide(note,70,30)note_glide(note,30,40,0.1)note_glide(note,40,70,0.1)synth.release(note)

Pitch bend, bend lfo

The above approach isn't very efficient. So far the best way I've found to dopitch-bend is to use an LFO on thenote.bend property, like withvibrato,but with a specially-constructed "line" LFO in one-shot mode.For a demo of the below code,see this post.

# ... synthio audio set up as normal ...defbend_note(note,start_notenum,end_notenum,bend_time=1):note.frequency=synthio.midi_to_hz(start_notenum)bend_amount= (end_notenum-start_notenum)/12# special two-point line LFO that goes from 0 to bend_amountbend_lfo=synthio.LFO(waveform=np.linspace(-16384,16383,num=2,dtype=np.int16),rate=1/bend_time,scale=bend_amount,offset=bend_amount/2,once=True)note.bend=bend_lfostart_notenum=40# E2end_notenum=52# E3whileTrue:print("start:",start_notenum,"end:",end_notenum)note=synthio.Note(synthio.midi_to_hz(start_notenum),panning=0 )synth.press(note)time.sleep(2)bend_note(note,start_notenum,end_notenum,0.75)synth.release(note)time.sleep(1)start_notenum=end_notenum# save end note so we can pick new end noteend_notenum=random.randint(22,64)

Note that in addition to passing in the start note number tosynthio.Note(),we must pass in the start note number and end MIDI note number tobend_note().

Waveforms

The default oscillator waveform insynthio.Synthesizer is a square-wave with 50% duty-cycle.But synthio will accept any buffer of data and treat it as a single-cycle waveform.One of the easiest ways to make the waveform buffers that synthio expects is to useulab.numpy.The numpy functions also have useful tools likenp.linspace() to generate a line through a number spaceand trig functions likenp.sin(). Once you have a waveform, set it with eithersynth.waveformor creating a newsynthio.Note(waveform=...)

Making your own waves

Here's an example that creates two new waveforms: a sine way and a sawtooth wave, and then playsthem a two-note chord, first with sine waves, then with sawtooth waves.

importboard,time,audiopwmio,synthioimportulab.numpyasnpaudio=audiopwmio.PWMAudioOut(board.GP10)synth=synthio.Synthesizer(sample_rate=22050)audio.play(synth)# create sine & sawtooth single-cycle waveforms to act as oscillatorsSAMPLE_SIZE=512SAMPLE_VOLUME=32000# 0-32767wave_sine=np.array(np.sin(np.linspace(0,2*np.pi,SAMPLE_SIZE,endpoint=False))*SAMPLE_VOLUME,dtype=np.int16)wave_saw=np.linspace(SAMPLE_VOLUME,-SAMPLE_VOLUME,num=SAMPLE_SIZE,dtype=np.int16)midi_note=65my_wave=wave_sawwhileTrue:# create notes using those waveformsnote1=synthio.Note(synthio.midi_to_hz(midi_note),waveform=my_wave)note2=synthio.Note(synthio.midi_to_hz(midi_note-7),waveform=my_wave)synth.press(note1)time.sleep(0.5)synth.press(note2)time.sleep(1)synth.release( (note1,note2) )time.sleep(0.1)my_wave=wave_sineifmy_waveiswave_sawelsewave_saw# toggle waveform

Wavetable morphing

One of the coolest things aboutsynthio being wavetable-based, is that we can alter thewaveformin real time!

Given the above setup but replacing the "while" loop, this will mix between the sine & square wave.

The trick here is that we give thesynthio.Note object an initial empty waveform bufferand then instead of replacing that buffer withnote.waveform = some_wave we copy withnote.waveform[:] = some_wave.

(This avoids needing an additionalnp.int16 result buffer forlerp(), since lerp-ing results in anp.float32 array)

[...hardwaresetupfromabove ...]# create sine & sawtooth single-cycle waveforms to act as oscillatorsSAMPLE_SIZE=512SAMPLE_VOLUME=32000# 0-32767wave_sine=np.array(np.sin(np.linspace(0,2*np.pi,SAMPLE_SIZE,endpoint=False))*SAMPLE_VOLUME,dtype=np.int16)wave_saw=np.linspace(SAMPLE_VOLUME,-SAMPLE_VOLUME,num=SAMPLE_SIZE,dtype=np.int16)# mix between values a and b, works with numpy arrays too,  t ranges 0-1deflerp(a,b,t):return (1-t)*a+t*bwave_empty=np.zeros(SAMPLE_SIZE,dtype=np.int16)# empty buffer we'll use array slice copy "[:]" onnote=synthio.Note(frequency=220,waveform=wave_empty)synth.press(note)pos=0whileTrue:print(pos)note.waveform[:]=lerp(wave_sine,wave_saw,pos)pos+=0.01ifpos>=1:pos=0time.sleep(0.01)

Filters

Filters let you change the character / timbre of the raw oscillator sound.The filter algorithm insynthio is a Biquad filter, giving a two-pole (12dB)low-pass (LP), high-pass (HP), or band-pass (BP) filters.

To set a filter at a fixed frequency, set theNote.filter propertyusing one of thesynthio.*_filter() methods:

[ ...synthiosetupasnormal ...]frequency=2000resonance=1.5lpf=synth.low_pass_filter(frequency,resonance)hpf=synth.high_pass_filter(frequency,resonance)bpf=synth.band_pass_filter(frequency,resonance)note1=synth.Note(frequency=220,filter=lpf)note2=synth.Note(frequency=330,filter=hpf)note3=synth.Note(frequency=440,filter=bpf)

Note that making a filter is a complex operation, requiring a function,and you cannot set the properties of a resulting filter after its created.This makes modulating the filter a bit trickier.

Also note that currently there are some glitchy instabilties in the filterwhen resonance is 2 or greater and filter frequency is close to note frequency.

Filter modulation

The standard synthio approach to modulation is to create asynthio.LFO and attach it to a property.(see above LFO examples) The properties must be of typesynthio.BlockInput for this to work, though.Not all synthio properties areBlockInputs, most notably, theNote.filter property.

So one way to modulate a filter is to use Python:

# fake a looping ramp down filter sweepfmin=100fmax=1000f=fmaxnote=synth.Note(frequency=220)synth.play(note)whileTrue:note.filter=synth.low_pass_filter(f,1.5)# adjust note's filterf=f-10iff<fmin:f=fmaxtime.sleep(0.01)# sleep determines our ramp rate

A more "synthio" way to modulate filter is to use an LFO but hand-copying LFO value to the filter.This requires adding the LFO to thesynth.blocksglobal runner since the LFO is not directly associated with aNote.

fmin=100fmax=1000ramp_down=np.array( (32767,0),dtype=np.int16)# unpolar ramp down, when interpolated by LFOf_lfo=synth.LFO(rate=0.3,scale=fmax-fmin,offset=fmin,waveform=ramp_down)synth.blocks.append(f_lfo)# add lfo to global LFO runner to get it to ticknote=synth.Note(frequency=220)synth.play(note)# start note soundingwhileTrue:note.filter=synth.low_pass_filter(f_lfo.value,1.5)# adjust its filtertime.sleep(0.001)

This is a fairly advanced technique as it requires keeping track of the LFO objects stuffedintosynth.blocks so they can be removed later. See "Keeping track of pressed notes" below forone technique for doing this.

Advanced Techniques

Keeping track of pressed notes

When passingsynthio.Note objects tosynth.press() instead of MIDI note numbers,your code must remmeber thatNote object so it can pass it intosynth.release() to stop it playing.

One way to do this is with a Python dict, where the key is whatever your unique identifier is(e.g. MIDI note number here for simplicity) and the value is the note object.

# setup as before to get `synth` & `midi` objectsnotes_pressed= {}# which notes being pressed. key=midi note, val=note objectwhileTrue:msg=midi.receive()ifisinstance(msg,NoteOn)andmsg.velocity!=0:# NoteOnnote=synthio.Note(frequency=synthio.midi_to_hz(msg.note),waveform=wave_saw,#..etc )synthio.press(note)notes_pressed[msg.note]=noteelifisinstance(msg,NoteOff)orisinstance(msg,NoteOn)andmsg.velocity==0:# NoteOffnote=notes_pressed.get(msg.note,None)# let's us get back None w/o try/exceptifnote:syntho.release(note)

Detuning oscillators for fatter sound

Since we have fine-grained control over a note's frequency withnote.frequency, this means we can do acommon technique for getting a "fatter" sound.

importboard,time,audiopwmio,synthioaudio=audiopwmio.PWMAudioOut(board.TX)synth=synthio.Synthesizer(sample_rate=22050)audio.play(synth)detune=0.005# how much to detune, 0.7% herenum_oscs=1midi_note=45whileTrue:print("num_oscs:",num_oscs)notes= []# holds note objs being pressed# simple detune, always detunes upforiinrange(num_oscs):f=synthio.midi_to_hz(midi_note)* (1+i*detune)notes.append(synthio.Note(frequency=f) )synth.press(notes)time.sleep(1)synth.release(notes)time.sleep(0.1)# increment number of detuned oscillatorsnum_oscs=num_oscs+1ifnum_oscs<5else1

Turn WAV files info oscillators

Thanks toadafruit_wave it is reallyeasy to load up a WAV file into a buffer and use it as a synthio waveform. Two great repositories ofsingle-cycle waveforms areAKWF FREEandwaveeditonline.com

# orig from @jepler 31 May 2023 1:34p #circuitpython-dev/synthioimportadafruit_wave# reads in entire wavedefread_waveform(filename):withadafruit_wave.open(filename)asw:ifw.getsampwidth()!=2orw.getnchannels()!=1:raiseValueError("unsupported format")returnmemoryview(w.readframes(w.getnframes())).cast('h')# this verion lets you lerp() it to mix w/ another wavedefread_waveform_ulab(filename):withadafruit_wave.open(filename)asw:ifw.getsampwidth()!=2orw.getnchannels()!=1:raiseValueError("unsupported format")returnnp.frombuffer(w.readframes(w.getnframes()),dtype=np.int16)my_wave=read_waveform("AKWF_granular_0001.wav")

Using WAV Wavetables

Thewaveeditonline.com site has specially constructed WAV filescalled "wavetables" that contain 64 single-cycle waveforms, each waveform having 256 samples.The waveforms in a wavetable are usually harmonically-related, so scanning through themcan produce interesting effects that could sound similar to using a filter,without needing to usesynth.filter!

The code below will load up one of these wavetables, and let you pick differentwaveforms within by settingwavetable.set_wave_pos(n).

importboard,time,audiopwmio,synthioimportulab.numpyasnpimportadafruit_waveaudio=audiopwmio.PWMAudioOut(board.GP10)synth=synthio.Synthesizer(sample_rate=22050)audio.play(synth)# mix between values a and b, works with numpy arrays too,  t ranges 0-1deflerp(a,b,t):return (1-t)*a+t*bclassWavetable:def__init__(self,filepath,wave_len=256):self.w=adafruit_wave.open(filepath)self.wave_len=wave_len# how many samples in each waveifself.w.getsampwidth()!=2orself.w.getnchannels()!=1:raiseValueError("unsupported WAV format")self.waveform=np.zeros(wave_len,dtype=np.int16)# empty buffer we'll copy intoself.num_waves=self.w.getnframes()/self.wave_lendefset_wave_pos(self,pos):"""Pick where in wavetable to be, morphing between waves"""pos=min(max(pos,0),self.num_waves-1)# constrainsamp_pos=int(pos)*self.wave_len# get sample positionself.w.setpos(samp_pos)waveA=np.frombuffer(self.w.readframes(self.wave_len),dtype=np.int16)self.w.setpos(samp_pos+self.wave_len)# one wave upwaveB=np.frombuffer(self.w.readframes(self.wave_len),dtype=np.int16)pos_frac=pos-int(pos)# fractional position between wave A & Bself.waveform[:]=lerp(waveA,waveB,pos_frac)# mix waveforms A & Bwavetable=Wavetable("BRAIDS02.WAV",256)# from http://waveeditonline.com/index-17.htmlnote=synthio.Note(frequency=220,waveform=wavetable.waveform)synth.press(note )# start an oscillator going# scan through the wavetable, morphing through each onei=0di=0.1# how fast to scanwhileTrue:i=i+diifi<=0ori>=wavetable.num_waves:di=-diwavetable.set_wave_pos(i)time.sleep(0.001)

Using LFO values in your own code

[tbd]

Usingsynthio.Math withsynthio.LFO

[tbd]

Drum synthesis

[tbd, but check outgamblor's drums.py gist]

Examples

Here are somelarger synthio-tricks examples with wiring diagrams.

Troubleshooting

Glitches whencode.py is saved or CIRCUITPY drive access

Distortion in audio

About

tips, tricks, and examples of using CircuitPython synthio

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

[8]ページ先頭

©2009-2025 Movatter.jp