Use
The CircuitPython code on the previous page makes the EP-2350 act similarly to the stock Teenage Engineering firmware and app. The device acts as a voice changing microphone that can store up to 4 presets with different effects chains to manipulate the voice audio. It can also play up to 4 wave sample files that can be sent through effects chains.
Here is a video that demonstrates the effects that are included in the config.json file in the project bundle.
Terms
The configuration this project supports is very dynamic and powerful, understanding a few key terms and concepts will make it much easier to experiment with and use to get the sound you're after.
- Pack - A config.json contains the configuration for a single pack. The pack encompasses the presets and samples. You can keep multiple pack config files on the CIRCUITPY drive and switch between them by renaming one to config.json and the others to alternate names. Preset effects are applied to the audio from the microphone before sending the modified audio out the 3.5mm jack.
- Preset - A preset is a list of effects and optionally blocks. A pack can contain up to 4 presets. The currently selected preset is indicated by the 4 top red LEDs on the front of the device.
- Sample - A sample is a wave audio file and optionally a list of effects and blocks used to modify it when played. A pack can contain up to 4 samples. The effects chains used on samples are separate from the ones in presets. The currently selected sample is indicated by the 4 bottom white LEDs on the device.
- Effect - An effect is a single item within an effect chain. In this CircuitPython implementation, the supported effects are the classes from the audiodelays, audiofilters, and audiofreeverb core modules. Effects can have their parameters set from the JSON config. Effect chains work top to bottom in the list. So each effect in the chain will modify the output of all of the effects that came before it.
-
Block - A block is a dynamic value that can be set as the parameter for an effect. The supported blocks are listed in the docs under synthio.BlockInput. LFOs, and math operations being the most interesting. They allow you to set up effect parameters to sweep a range of values over time. Blocks can be included inline as the value of a paremeter, or can be put into a
"blocks"section of the preset or sample and referred to by a given name using prepended dollar sign syntax like$sweep.
The basic controls are shown in the photo to the left.
- The orange top side button changes the selected preset. Currently selected preset is indicated by the top 4 red LEDs.
- The green middle side button changes the selected sample. Currently selected sample is indicated by the bottom 4 white LEDs.
- The white bottom side button triggers the current sample to play according to its playmode.
- Pressing the handle activates the voice passthrough with the currently selected preset's effects. If no preset is selected then it will be a clean passthrough.
To turn the device off, press and hold both the orange top side button, and green middle side button for 1 second. All LEDs will shut off and the device will power off.
To turn the device back on, press the handle all the way in and wait for the top white LED to blink before releasing.
Configuration Tool
Editing JSON by hand is tedious and error prone. To make the configuration process easier here is a web page GUI configuration tool (a similar tool exists for the stock MicroPython firmware here). This page allows you set all available configurations using standard UI controls. Once everything is set you can easily copy from the page to the config.json file on your CIRCUITPY drive.
This video provides a short tutorial on how to use the config tool.
The GUI config editor linked above is the easiest way to configure the device without having to worry about JSON syntax and typos breaking the config. But if you want to better understand how the JSON is structured or make modifications to it by hand the following section details the syntax.
Config Syntax
The config.json syntax used by the CircuitPython code is similar conceptually the config supported by the original TE firmware and app. CircuitPython has different effects and in some cases different parameter names, so the config is not a drop-in 1 to 1 replacement. However, it should feel very familiar to you if you have experience configuring the device in it's standard out-of-box state.
Pack
A config.json represents a single pack. The basic structure is a JSON dictionary with "name", "presets" and "samples" keys. name is a string that you can set as a reminder to yourself what the pack is. presets is a list of up to 4 preset definitions. samples is a list of up to 4 sample definitions.
{
"name": "DEMOPACK",
"presets": [
...
],
"samples": [
...
]
}
Preset
A preset is defined by a JSON dictionary containing a "list" key that holds a list of effects. You can also optionally include a "name", and/or "comment" key with strings containing a human readable note about the preset to jog your memory later. The "blocks" key can also be included to define a dictionary of named blocks which are used to change parameter values dynamically.
{
"name": "Robot Pitch Shift",
"comment": "Make you sound like a chipmunk robot",
"list": [
...
],
"blocks": {
...
}
},
Sample
A sample is defined by a JSON dictionary that contains at minimum "file", and "playmode" keys. File is the filepath to a wave file on the CIRCUITPY drive for the sample. Playmode is one of the following:
-
oneshot- Plays the sample wave file once in full per button press trigger. -
hold- Plays the sample wave file on a loop while the trigger button is held down. Cuts off immediately when released, even if the full sample has not played. -
startstop- Starts playing the sample wave file on a loop when you press the trigger button once, and stops playing it when you press the trigger button a second time.
The sample definition can also optionally include "effects" and "blocks" keys. They hold a list of effects to apply to the sample, and blocks for dynamic parameters. They're syntax is the same as the "list" and "blocks" keys from the preset definition.
Here is an example of a sample with some effects.
{
"file": "2.wav",
"playmode": "hold",
"effects": [
{
"effect": "chorus",
"max_delay_ms": 350,
"delay_ms": 100,
"voices": 3,
"mix": 0.85
}
]
},
Effect
An effect is defined by a JSON dictionary with "effect" and "mix" keys. mix is a float value from 0 to 1.0 that declares how strong the effect will be in the output. 1.0 is full effect, and 0.0 is no effect. The "effect" gets set to a string value naming the effect it should be. One of the following:
chorusechogranular_pitch_shiftmulti_tap_delaypitch_shiftdistortionfilterphaserfreeverb
Each one supports a different set of additional parameters to control how it will modify sound. See documentation for the audiodelays, audiofilters, and audiofreeverb modules for a comprehensive reference. Each of the above corresponds with a class in one of these modules.
Here is an example of an effect definition with parameters.
{
"effect": "distortion",
"drive": 0.6,
"pre_gain": 12,
"post_gain": -8,
"mode": "overdrive",
"soft_clip": true,
"mix": 1
},
Block
Blocks can be declared in-line within the value of a parameter, or as a named entry in the "blocks" dictionary.
A named block is defined by a JSON key/value pair with a name for a key and a value that holds a dictionary having the "block" key at a minimum, plus any other parameters needed to configure the specific block type. See the documentation for synthio.BlockInput to find parameters used by lfo and math, the two supported dynamic types of block. An in-line block uses the same syntax for the block definition, it just lacks a name.
Here is an example of an named LFO block:
"blocks": {
"semitone_sweep": {
"block": "lfo",
"waveform": "square",
"rate": 3,
"scale": 12.0,
"offset": 0
}
},
The block above, named semitone_sweep, oscillates with a square wave from -12 to +12 at 3hz, or 3 times per second.
Here is an example of an in-line math block that uses the special $handle value which maps to the analog position of the handle on the device:
{
"effect": "pitch_shift",
"semitones": {
"block": "math",
"operation": "constrained_lerp",
"a": -12,
"b": 12,
"c": "$handle"
},
"mix": 1,
"window": 1024,
"overlap": 128
},
The above example shows a full effect definition with a math block declared in-line for the semitones value. This math block uses linear interpolation to map the 0.0 to 1.0 values from $handle to a range of -12 to +12 and uses that for the semitones of the pitch shift. The result is the handle on the device giving analog control over the pitch. Squeeze the handle only a little get very low pitch, squeeze it half way for normal pitch, and squeeze it all the way for very high pitch.
Sample Config
The project bundle includes this sample configuration file that demonstrates basic and advanced configuration techniques. Reading over it can give you ideas for your own packs. This config can be imported into the GUI config editor as well to start tweaking and changing from it instead of a blank slate.
{
"name": "DEMOPACK",
"presets": [
{
"list": [
{
"effect": "pitch_shift",
"semitones": {"block":"math","operation":"constrained_lerp","a":-12,"b":12,"c":"$handle"},
"mix": 1,
"window": 1024,
"overlap": 128
},
{
"effect": "distortion",
"drive": 0.6,
"pre_gain": 12,
"post_gain": -8,
"mode": "overdrive",
"soft_clip": true,
"mix": 1
},
{"effect":"filter","filter":{"mode":"low_pass","frequency":3500,"Q":0.7071},"mix":1}
]
},
{
"list": [
{"effect":"filter","filter":{"mode":"high_pass","frequency":200,"Q":0.7071},"mix":1},
{"effect": "granular_pitch_shift", "semitones": 8.0, "density": 3.0},
{"effect":"echo","max_delay_ms":1200,"delay_ms":750,"decay":0.45,"mix":0.35,"freq_shift":true},
{"effect":"freeverb","roomsize":0.75,"damp":0.55,"mix":0.6}
]
},
{
"list": [
{"effect":"chorus","max_delay_ms":50,"delay_ms":25,"voices":3,"mix":0.5},
{"effect":"phaser","frequency":600,"feedback":0.7,"mix":0.5,"stages":8},
{
"effect": "multi_tap_delay",
"max_delay_ms": 750,
"delay_ms": 500,
"decay": 0.6,
"mix": 0.3,
"taps": [
[0.333,0.6],
[0.666,0.8],
1
]
}
]
},
{
"blocks": {
"sweep": {"block":"lfo","waveform":"sine","rate":1.5,"scale":1000,"offset":2000},
"delay_sweep": {"block":"lfo","waveform":"sine","rate":1,"scale":100,"offset": 200}
},
"list": [
{"effect":"pitch_shift","semitones":-6,"mix":1,"window":1024,"overlap":128},
{"effect":"chorus","max_delay_ms":450,"delay_ms":"$delay_sweep","voices":3,"mix":0.65},
{"effect":"phaser","frequency":"$sweep","feedback":0.7,"mix":0.5,"stages":6}
]
}
],
"samples": [{
"file": "1.wav",
"playmode": "startstop",
"blocks": {
"semitone_sweep": {
"block": "lfo",
"waveform": "square",
"rate": 3,
"scale": 12.0,
"offset": 0
}
},
"effects": [
{
"effect": "granular_pitch_shift",
"semitones": "$semitone_sweep",
"density": 3.0
}
]
},
{
"file": "2.wav",
"playmode": "hold",
"effects": [
{"effect":"chorus","max_delay_ms":350,"delay_ms":100,"voices":3,"mix":0.85}
]
},
{
"file": "3.wav",
"playmode": "oneshot",
"effects": [
{"effect":"filter","filter":{"mode":"high_pass","frequency":1200,"Q":0.7071},"mix":1},
{"effect": "granular_pitch_shift", "semitones": -8.0, "density": 3.0},
{"effect":"freeverb","roomsize":0.75,"damp":0.55,"mix":0.6}
]
},
{
"file": "4.wav",
"playmode": "oneshot",
"effects": [
{
"effect": "filter",
"filter": {
"mode": "high_pass",
"frequency": 1200,
"Q": 0.7071
},
"mix": 1
},
{
"effect": "granular_pitch_shift",
"semitones": 12.0,
"density": 3.0
},
{
"effect": "freeverb",
"roomsize": 0.55,
"damp": 0.55,
"mix": 0.6
}
]
}
]
}
Page last edited August 05, 2026
Text editor powered by tinymce.