Code Explanation
This page contains an overview how the code works, and what different sections are responsible for. It's written in top down order following along with code.py. To get the most out of it look at the contents of code.py side-by-side with this page.
Configurable Variables
The first thing underneath the imports is a handful of variables that can be used to configure the behavior of the app.
-
OUTPUT_PATH- Where the sample wave file will get saved to. /sd/sample0.wav by default. -
SONG_PATH- The MIDI file to use for automated playback. anna-magdalena-20a.mid by default. -
CC_RECORD- The CC button value to use for the record button.77by default -
CC_PLAY_SONG- The CC button value to use for the automatic MIDI file playback.78by default. -
SONG_PLAYBACK_BPM- The approximate beats per minute to play the MIDI file at.135by default.
CC Debouncing
My keyboard always sends duplicate events when the CC buttons are pressed. The code ignores the duplicates by implementing a debouncing mechanism with cooldown timer. The default time to ignore duplicate events is set by CC_DEBOUNCE_DEFAULT. Individual CC actions can override the default by adding entries to the CC_COOLDOWNS dictionary. There is a helper function, cc_debounced(), that enforces the cooldown timers.
USB MIDI Keyboard Setup
To find the MIDI keyboard, the code loops over all connected USB host devices and attempts to initialize them using the adafruit_usb_host_midi library. Once found, the keyboard is hooked into the adafruit_midi library in order to parse the MIDI events received via USB host.
DAC Setup
The Fruit Jam's on-board TLV320 DAC is initialized. The DAC is configured to output via the 3.5mm headphone jack. The volume is set here as well, adjust the values as needed if your speakers don't have hardware volume control. The I2SOut instance is stored in the audio variable.
Polyphony Setup
The audiomixer module is used to handle polyphony. A Mixer object is instantiated with voice_count=4, meaning it can play up to four things layered at once. The mixer is played on the I2SOut audio variable, so all 4 voices will play from I2S DAC headphone output.
General Vars & Sample Setup
Next are 3 variables used by different parts of the code. effect_chains is a list that will hold four copies of the effects chain, one per voice in the mixer. samples is a list that will hold the RawSample instances of the unmodified audio sample, there will be four copies of it, one per voice in the mixer. recording_file will hold the opened file reference to the wave file that is being written during recording.
DataContext is a class that holds 4 more variables. They are contained within a class to avoid the need to use the global keyword in functions that update the value of these variables.
-
data_start- Offset to the location within the wave file of the section that contains raw audio data. -
pcm- Amemoryviewthat will hold the raw audio data extracted from the wave file. -
cur_voice_index- The index of the voice within the mixer to use for playing the next sample. It will get automatically incremented whenever a sample is played. As you play more notes it will cycle between the 4 available voices within the mixer. -
song_player- ASamplerMIDIPlayerinstance that will handle parsing and playing the song from the MIDI file.
The load_samples() function is responsible for loading the sample from the wave file on the SD card. It will populate the 4 RawSamples inside of the samples list.
Effects Chain Setup
4 copies of the effects chain are initialized and stored in the effects_chains list. Each copy includes two effects:
-
amp- An instance ofaudiofilters.Distortionwith thedriveset to0.0, meaning it won't add any actual distortion. Thepre_gainargument on it is used to boost the volume of the sample. This is helpful because the microphone recordings tend to be quiet. -
gran_pitch_shift- An instance ofaudiodelays.GranularPitchShift. This is used to apply the change in pitch for each of the notes on the keyboard. When different keys are pressed, thesemitonesproperty is updated to an offset based on the key's note value.
There are 4 copies of the effects chain so that each voice in the audiomixer can have it's own chain to work with independently.
Play Note & Song Functions
The trigger_note() function will play the note specified by the value of note argument. It is called when the keys on the keyboard are pressed. It's also used by the automatic MIDI file playback. It will automatically select the correct voice within the audiomixer, and increment data_context.cur_voice_index in preparation for the next note.
A SamplerMIDIPlayer class is defined that extends adafruit_midi_parser.MIDIPlayer. The class overrides the on_note_on() function to use the trigger_note() function. When paired with the MIDIParser on_note_on() will get called automatically as appropriate based on the song in the MIDI file and BPM.
The start_song() function initializes the MIDIParser and uses it along with the instance of SamplerMIDIPlayer to begin playing the MIDI file song. On subsequent calls, after the objects are already initialized, it will instead just stop and rewind the player back to the beginning of the song.
Main Loop
The main loop of the code boils down to polling the USB MIDI keyboard for new events and handling any that are received. The code uses a basic state machine with a few different states. The current state is kept in the STATE variable. In the normal "playing" state, when keyboard key events are recieved it will call trigger_note() for the note pressed.
When CC events are recieved it will check if they are the defined CC_RECORD, or CC_PLAY_SONG values. For the CC_PLAY_SONG button it will call the start_song() function.
For CC_RECORD it changes the state to "recording_prompt" and sets a NeoPixel to yellow. In the "recording_prompt" state the standard keyboard keys have different behavior, instead of playing the sample, pressing a key will start the live recording and releasing the key stops the recording. While recording the NeoPixels changes from yellow to red. When recording stops, the NeoPixel is turned off.
Page last edited July 20, 2026
Text editor powered by tinymce.