Create cpx-basic-synth.py
This commit is contained in:
parent
366b0da93e
commit
e4631b5c1f
1 changed files with 217 additions and 0 deletions
217
Circuit_Playground_Express_USB_MIDI/cpx-basic-synth.py
Normal file
217
Circuit_Playground_Express_USB_MIDI/cpx-basic-synth.py
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
### cpx-basic-synth v1.4
|
||||
### CircuitPython (on CPX) synth module using internal speaker
|
||||
### Velocity sensitive monophonic synth
|
||||
### with crude amplitude modulation (cc1) and choppy pitch bend
|
||||
|
||||
### Tested with CPX and CircuitPython and 4.0.0-beta.7
|
||||
|
||||
### Needs recent adafruit_midi module
|
||||
|
||||
### copy this file to CPX as code.py
|
||||
|
||||
### MIT License
|
||||
|
||||
### Copyright (c) 2019 Kevin J. Walters
|
||||
|
||||
### Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
### of this software and associated documentation files (the "Software"), to deal
|
||||
### in the Software without restriction, including without limitation the rights
|
||||
### to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
### copies of the Software, and to permit persons to whom the Software is
|
||||
### furnished to do so, subject to the following conditions:
|
||||
|
||||
### The above copyright notice and this permission notice shall be included in all
|
||||
### copies or substantial portions of the Software.
|
||||
|
||||
### THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
### IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
### FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
### AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
### LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
### OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
### SOFTWARE.
|
||||
|
||||
import array
|
||||
import time
|
||||
import math
|
||||
|
||||
import digitalio
|
||||
import audioio
|
||||
import board
|
||||
import usb_midi
|
||||
import neopixel
|
||||
|
||||
import adafruit_midi
|
||||
|
||||
from adafruit_midi.midi_message import note_parser
|
||||
|
||||
from adafruit_midi.note_on import NoteOn
|
||||
from adafruit_midi.note_off import NoteOff
|
||||
from adafruit_midi.control_change import ControlChange
|
||||
from adafruit_midi.pitch_bend import PitchBend
|
||||
|
||||
# Turn the speaker on
|
||||
speaker_enable = digitalio.DigitalInOut(board.SPEAKER_ENABLE)
|
||||
speaker_enable.direction = digitalio.Direction.OUTPUT
|
||||
speaker_on = True
|
||||
speaker_enable.value = speaker_on
|
||||
|
||||
dac = audioio.AudioOut(board.SPEAKER)
|
||||
|
||||
# 440Hz is the standard frequency for A4 (A above middle C)
|
||||
# MIDI defines middle C as 60 and modulation wheel is cc 1 by convention
|
||||
A4refhz = const(440)
|
||||
midi_note_C4 = note_parser("C4")
|
||||
midi_note_A4 = note_parser("A4")
|
||||
midi_cc_modwheel = const(1)
|
||||
twopi = 2 * math.pi
|
||||
|
||||
# A length of 12 will make the sawtooth rather steppy
|
||||
sample_len = 12
|
||||
base_sample_rate = A4refhz * sample_len
|
||||
max_sample_rate = 350000 # a CPX / M0 DAC limitation
|
||||
|
||||
midpoint = 32768
|
||||
|
||||
# A sawtooth function like math.sin(angle)
|
||||
# 0 returns 1.0, pi returns 0.0, 2*pi returns -1.0
|
||||
def sawtooth(angle):
|
||||
return 1.0 - angle % twopi / twopi * 2
|
||||
|
||||
# make a sawtooth wave between +/- each value in volumes
|
||||
# phase shifted so it starts and ends near midpoint
|
||||
# "H" arrays for RawSample looks more memory efficient
|
||||
# see https://forums.adafruit.com/viewtopic.php?f=60&t=150894
|
||||
def waveform_sawtooth(length, waves, volumes):
|
||||
for vol in volumes:
|
||||
waveraw = array.array("H",
|
||||
[midpoint +
|
||||
round(vol * sawtooth((idx + 0.5) / length
|
||||
* twopi
|
||||
+ math.pi))
|
||||
for idx in list(range(length))])
|
||||
waves.append((audioio.RawSample(waveraw), waveraw))
|
||||
|
||||
# Make some square waves of different volumes volumes, generated with
|
||||
# n=10;[round(math.sqrt(x)/n*32767*n/math.sqrt(n)) for x in range(1, n+1)]
|
||||
# square root is for mapping velocity to power rather than signal amplitude
|
||||
# n=15 throws MemoryError exceptions when a note is played :(
|
||||
waveform_by_vol = []
|
||||
waveform_sawtooth(sample_len,
|
||||
waveform_by_vol,
|
||||
[10362, 14654, 17947, 20724, 23170,
|
||||
25381, 27415, 29308, 31086, 32767])
|
||||
|
||||
# brightness 1.0 saves memory by removing need for a second buffer
|
||||
# 10 is number of NeoPixels on CPX
|
||||
numpixels = const(10)
|
||||
pixels = neopixel.NeoPixel(board.NEOPIXEL, numpixels, brightness=1.0)
|
||||
|
||||
# Turn NeoPixel on to represent a note using RGB x 10
|
||||
# to represent 30 notes - doesn't do anything with pitch bend
|
||||
def noteLED(pix, pnote, pvel):
|
||||
note30 = (pnote - midi_note_C4) % (3 * numpixels)
|
||||
pos = note30 % numpixels
|
||||
r, g, b = pix[pos]
|
||||
if pvel == 0:
|
||||
brightness = 0
|
||||
else:
|
||||
# max brightness will be 32
|
||||
brightness = round(pvel / 127 * 30 + 2)
|
||||
# Pick R/G/B based on range within the 30 notes
|
||||
if note30 < 10:
|
||||
r = brightness
|
||||
elif note30 < 20:
|
||||
g = brightness
|
||||
else:
|
||||
b = brightness
|
||||
pix[pos] = (r, g, b)
|
||||
|
||||
# Calculate the note frequency from the midi_note with pitch bend
|
||||
# of pb_st (float) semitones
|
||||
# Returns float
|
||||
def note_frequency(midi_note, pb_st):
|
||||
# 12 semitones in an octave
|
||||
return A4refhz * math.pow(2, (midi_note - midi_note_A4 + pb_st) / 12.0)
|
||||
|
||||
midi_channel = 1
|
||||
midi = adafruit_midi.MIDI(midi_in=usb_midi.ports[0],
|
||||
in_channel=midi_channel-1)
|
||||
|
||||
# pitchbendrange in semitones - often 2 or 12
|
||||
pb_midpoint = 8192
|
||||
pitch_bend_multiplier = 2 / pb_midpoint
|
||||
pitch_bend_value = pb_midpoint # mid point - no bend
|
||||
|
||||
wave = [] # current or last wave played
|
||||
last_note = None
|
||||
|
||||
# Amplitude modulation frequency in Hz
|
||||
am_freq = 16
|
||||
mod_wheel = 0
|
||||
|
||||
# Read any incoming MIDI messages (events) over USB
|
||||
# looking for note on, note off, pitch bend change
|
||||
# or control change for control 1 (modulation wheel)
|
||||
# Apply crude amplitude modulation using speaker enable
|
||||
while True:
|
||||
msg = midi.receive()
|
||||
if isinstance(msg, NoteOn) and msg.velocity != 0:
|
||||
last_note = msg.note
|
||||
# Calculate the sample rate to give the wave form the frequency
|
||||
# which matches the midi note with any pitch bending applied
|
||||
pitch_bend = (pitch_bend_value - pb_midpoint) * pitch_bend_multiplier
|
||||
note_freq = note_frequency(msg.note, pitch_bend)
|
||||
note_sample_rate = round(base_sample_rate * note_freq / A4refhz)
|
||||
|
||||
# Select the wave with volume for the note velocity
|
||||
# Value slightly above 127 together with int() maps the velocities
|
||||
# to equal intervals and avoids going out of bound
|
||||
wave_vol = int(msg.velocity / 127.01 * len(waveform_by_vol))
|
||||
wave = waveform_by_vol[wave_vol]
|
||||
|
||||
if note_sample_rate > max_sample_rate:
|
||||
note_sample_rate = max_sample_rate
|
||||
wave[0].sample_rate = note_sample_rate # must be integer
|
||||
dac.play(wave[0], loop=True)
|
||||
|
||||
noteLED(pixels, msg.note, msg.velocity)
|
||||
|
||||
elif (isinstance(msg, NoteOff) or
|
||||
isinstance(msg, NoteOn) and msg.velocity == 0):
|
||||
# Our monophonic "synth module" needs to ignore keys that lifted on
|
||||
# overlapping presses
|
||||
if msg.note == last_note:
|
||||
dac.stop()
|
||||
last_note = None
|
||||
|
||||
noteLED(pixels, msg.note, 0) # turn off NeoPixel
|
||||
|
||||
elif isinstance(msg, PitchBend):
|
||||
pitch_bend_value = msg.pitch_bend # 0 to 16383
|
||||
if last_note is not None:
|
||||
pitch_bend = (pitch_bend_value - pb_midpoint) * pitch_bend_multiplier
|
||||
note_freq = note_frequency(last_note, pitch_bend)
|
||||
note_sample_rate = round(base_sample_rate * note_freq / A4refhz)
|
||||
if note_sample_rate > max_sample_rate:
|
||||
note_sample_rate = max_sample_rate
|
||||
wave[0].sample_rate = note_sample_rate # must be integer
|
||||
dac.play(wave[0], loop=True)
|
||||
|
||||
elif isinstance(msg, ControlChange):
|
||||
if msg.control == midi_cc_modwheel:
|
||||
mod_wheel = msg.value # msg.value is 0 (none) to 127 (max)
|
||||
|
||||
if mod_wheel > 0:
|
||||
t1 = time.monotonic() * am_freq
|
||||
# Calculate a form of duty_cycle for enabling speaker for crude
|
||||
# amplitude modulation. Empirically the divisor needs to greater
|
||||
# than 127 as can't hear much when speaker is off more than half
|
||||
# 220 works reasonably well
|
||||
new_speaker_on = (t1 - int(t1)) > (mod_wheel / 220)
|
||||
else:
|
||||
new_speaker_on = True
|
||||
|
||||
if speaker_on != new_speaker_on:
|
||||
speaker_enable.value = new_speaker_on
|
||||
speaker_on = new_speaker_on
|
||||
Loading…
Reference in a new issue