
This note describes the process of searching for a way to output Eurorack CV (control voltage) signals from synthio via an I2S DAC that is connected to a UM FeatherS2.
Problem Statement: When using a CV-like object in synthio such as Envelope and LFO to control a Note's amplitude
, the resultant output is placed above the baseline voltage and only responds to positive control values. For example, a sine-wave LFO created to span negative and positive values to modulate a note's frequency
is rectified if used to modulate a note's amplitude
. This becomes important when an analog output of just the Envelope or LFO is needed rather than the modulated sound of the Note oscillator.
Here's the test setup:
- Create a wave shape table containing the maximum wave value (16-bit signed). Set the wave shape oscillator to an arbitrary frequency value such as 440Hz.
- Define the ADSR envelope or LFO.
- Define a
synthio.Note
object where theamplitude
parameter is controlled by the ADSR envelope or LFO. - "Press" the note to output the ADSR envelope or LFO signal via the I2S DAC connection.
Also successfully tested creating the CV signal using audiopwmio
and audioio
to connect to PWM and analog output pins on a Qt PY RP2040 and Grand Central M4 Express board.
Test Code
# SPDX-FileCopyrightText: 2023 JG for Cedar Grove Maker Studios # SPDX-License-Identifier: MIT import time import ulab.numpy as np import board import audiobusio # import audioio # import audiopwmio import audiomixer import synthio SAMPLE_RATE = 44100 SAMPLE_SIZE = 256 loudness = 1 VOLUME = int(loudness * 32760) # 0-32767 (signed 16-bit) # waveforms, envelopes and synth setup sine = np.array( np.sin(np.linspace(0, 4 * np.pi, SAMPLE_SIZE, endpoint=False)) * VOLUME, dtype=np.int16, ) dc_max = np.array([VOLUME for i in range(SAMPLE_SIZE)], dtype=np.int16) dc_min = np.array([-VOLUME for i in range(SAMPLE_SIZE)], dtype=np.int16) lfo = synthio.LFO(rate=0.5, waveform=sine) amp_env0 = synthio.Envelope( attack_time=0.25, decay_time=1, release_time=0.2, attack_level=1, sustain_level=0.5 ) synth = synthio.Synthesizer(sample_rate=SAMPLE_RATE) # ADSR envelope test # note_0 = synthio.Note(frequency=440, envelope=amp_env0, waveform=dc_max) # LFO test note_0 = synthio.Note(frequency=440, amplitude=lfo, waveform=dc_max) # Instantiate output path # I2S audio = audiobusio.I2SOut(bit_clock=board.D12, word_select=board.D9, data=board.D6) # Analog DAC # audio = audioio.AudioOut(board.A0) # PWM pin # audio = audiopwmio(board.D13) mixer = audiomixer.Mixer( voice_count=4, sample_rate=SAMPLE_RATE, channel_count=1, bits_per_sample=16, samples_signed=True, buffer_size=2048, ) audio.play(mixer) mixer.voice[0].play(synth) mixer.voice[0].level = 0.75 print("Test CV Output") while True: print("press note_0", note_0) synth.press(note_0) time.sleep(10) # 10s for LFO, 1s for ADSR envelope print("release note_0") synth.release(note_0) time.sleep(1)
Results:
The following oscilloscope photos were taken for each of the output mode tests. The photo on the left is the ADSR envelope signal. The right-hand photo is of the sine wave LFO signal.
I2S DAC Output

The I2S DAC output's envelope and LFO signals worked as expected. Both signals exist above the midpoint of the output which is 0 volts when using the I2S DAC. The sine wave LFO signal is rectified since the Note.amplitude
parameter can only accept positive values (at least for now). The thickness of the oscilloscope trace is due to the I2S DAC's internal ~2.7MHz negative voltage charge pump -- and likely won’t be seen by most CV inputs.
PWM Output

The PWM pin's envelope and VFO signals also worked as expected, residing above the midpoint of the output which is 1.65 volts. As in the previous test, the sine wave LFO signal is rectified since the Note.amplitude
parameter can only accept positive values. The thickness of the oscilloscope trace is a result of the simple RC low-pass filter on the output pin that passes a tiny amount of the PWM carrier frequency.
Analog DAC Output

The analog DAC pin's envelope and VFO signals worked as expected as well. The signals were positive with regard to the midpoint of the output which is 1.65 volts when using the an analog DAC output pin. Note that the sine wave LFO signal is rectified the same as the other two output tests. No power supply or PWM carrier frequency noise exists for the analog DAC output signal.
Recommendations for changes to synthio for CV:
The tests were all successful and produced viable and useful CV outputs for controlling external Eurorack modules, albeit at a lower overall voltage and with a midpoint offset. The rectified LFO output is problematic since it should be allowed to span the output midpoint voltage, particularly considering the I2S DAC is capable of outputting negative voltages.
- To resolve the midpoint bias issue, change the
Note.amplitude
parameter range fromZERO
andNEAR_ONE
to-NEAR_ONE
andNEAR_ONE
. PositiveNote
output values will work the same as before; negative values will result in an inverted signal of proportional amplitude (180-degrees out-of-phase). See Pull Request #1 below for details. - Create a
synthio.CV
object that operates similarly tosynthio.Note
so that CV signals can be altered by other CV signals, LFOs, mixers, and filters. This would approach would permit tuning the UI to include parameters strictly for CV signals (such as offset, phase, and range) and could reduce the potential confusion created by changing the range of allowableNote.amplitude
values. - Optional: Add a
.CVOutput
class toaudioio
,audiopwmio
, andaudiobusio
for instantiating analog DAC pin output, PWM pin output, or I2S DAC output. Not certain that this is actually needed since the existing approach could be used to define the CV output path.
Attribution: Patch Symbols from PATCH & TWEAK by Kim Bjørn and Chris Meyer, published by Bjooks, are licensed under Creative Commons CC BY-ND 4.0