Code Walkthrough

Code Walkthrough

As usual, we start with imports, hardware setup, and initialization of global constants.  OK, SAMPLES isn't literal constant, but it gets filled from the soundboard.txt file and is treated as a constant after that.

Download: file
import time
import board
import audioio
import adafruit_trellism4
from color_names import *

# Our keypad + neopixel driver
trellis = adafruit_trellism4.TrellisM4Express(rotation=0)


SELECTED_COLOR = WHITE            # the color for the selected sample
SAMPLE_FOLDER = '/samples/'       # the name of the folder containing the samples
SAMPLES = []

The next thing is to read the contents of SAMPLES from the soundboard.txt file.

We iterate through the file, line by line. If the line is empty, or it's first character is # (marking it as a comment) it gets ignored. If it is only the string pass, an empty element is appended to SAMPLES. Otherwise it is split in two at the first comma. Each piece has any whitespace trimmed from the ends before being assembled into a tuple and appended to the SAMPLES list. Notice how the eval function is used to convert the string representing the color to the appropriate value (either number or RGB tuple depending on the form of the string).

Download: file
with open('soundboard.txt', 'r') as f:
    for line in f:
        cleaned = line.strip()
        if len(cleaned) > 0 and cleaned[0] != '#':
            if cleaned == 'pass':
                SAMPLES.append(('does_not_exist.wav', BLACK))
            else:
                f_name, color = cleaned.split(',', 1)
                SAMPLES.append((f_name.strip(), eval(color.strip())))

It was mentioned earlier that all files need to have the same format. That format is determined by examining the first sample.

Download: file
channel_count = None
bits_per_sample = None
sample_rate = None
with open(SAMPLE_FOLDER+SAMPLES[0][0], 'rb') as f:
    wav = audioio.WaveFile(f)
    channel_count = wav.channel_count
    bits_per_sample = wav.bits_per_sample
    sample_rate = wav.sample_rate
    print('%d channels, %d bits per sample, %d Hz sample rate ' %
          (wav.channel_count, wav.bits_per_sample, wav.sample_rate))

    # Audio playback object - we'll go with either mono or stereo depending on
    # what we see in the first file
    if wav.channel_count == 1:
        audio = audioio.AudioOut(board.A1)
    elif wav.channel_count == 2:
        audio = audioio.AudioOut(board.A1, right_channel=board.A0)
    else:
        raise RuntimeError('Must be mono or stereo waves!')

Since one of our goals is to have a background loop over which we can play shot sound clips on demand, we need to use the mixer instead of the raw audio object.

A mixer object is created with the same sample settings as the audio object, with 2 channels. The audio object is then told to play what comes out of the mixer.

Download: file
mixer = audioio.Mixer(voice_count=2,
                      sample_rate=sample_rate,
                      channel_count=channel_count,
                      bits_per_sample=bits_per_sample,
                      samples_signed=True)
audio.play(mixer)

Once we have  the audio taken care of, we turn to the buttons. Each audio file specified in the soundboard.txt file (or the non-existant does_not_exist.wav where pass was specified) is checked. If it doesn't exist or it doesn't match the parameters fetched previously from the first file, that button is unlit. Otherwise it's color is set to that specified in soundboard.txt.

Download: file
trellis.pixels.fill(0)

for i, v in enumerate(SAMPLES):
    filename = SAMPLE_FOLDER+v[0]
    try:
        with open(filename, 'rb') as f:
            wav = audioio.WaveFile(f)
            print(filename,
                  '%d channels, %d bits per sample, %d Hz sample rate ' %
                  (wav.channel_count, wav.bits_per_sample, wav.sample_rate))
            if wav.channel_count != channel_count:
                pass
            if wav.bits_per_sample != bits_per_sample:
                pass
            if wav.sample_rate != sample_rate:
                pass
            trellis.pixels[(i % 8, i // 8)] = v[1]
    except OSError:
        # File not found! skip to next
        pass

Whenever a sound is played, it's details are captured in a structure. That structure is used in the stop_playing_sample function:

Download: file
def stop_playing_sample(details):
    print('playing: ', details)
    mixer.stop_voice(details['voice'])
    trellis.pixels[details['neopixel_location']] = details['neopixel_color']
    details['file'].close()
    details['voice'] = None

After some initialization we now come to the main loop. Current_press keeps track of what button(s) are currently pressed. This is used to determine what buttons have been pressed or released since the last time through the loop. Add a short delay at the end of the loop and this amounts to a form of budget debouncing.

We have a structure for the background loop as well as any other sound that gets played.

Download: file
current_press = set()
current_background = {'voice' : None}
currently_playing = {'voice' : None}
while True:

The first thing that happens in the loop is figuring out what buttons were pressed since last time.

Download: file
    pressed = set(trellis.pressed_keys)
    just_pressed = pressed - current_press

Now we loop through the buttons that have been pressed.

Based on the button's location in the grid, we figure out which sample it corresponds to. We try to open that file. If there's a problem doing so, the except clause ignores the attempt to play it.

If the button is in the top row, it is a background loop. This is played on voice 0 of the mixer, and set to loop repeatedly. If it's on any other row, it plays once on mixer voice 1. Any file is currently open/playing on the associated voice is first stopped and closed.

At the end of the loop, we delay briefly and update current_press for next time. 

Download: file
for down in just_pressed:
        sample_num = down[1]*8 + down[0]
        try:
            filename = SAMPLE_FOLDER+SAMPLES[sample_num][0]
            f = open(filename, 'rb')
            wav = audioio.WaveFile(f)

            if down[1] == 0:              # background loop?
                if current_background['voice'] != None:
                    stop_playing_sample(current_background)

                trellis.pixels[down] = WHITE
                mixer.play(wav, voice=0, loop=True)
                current_background = {
                    'voice': 0,
                    'neopixel_location': down,
                    'neopixel_color': SAMPLES[sample_num][1],
                    'sample_num': sample_num,
                    'file': f}
            else:
                if currently_playing['voice'] != None:
                    stop_playing_sample(currently_playing)

                trellis.pixels[down] = WHITE
                mixer.play(wav, voice=1, loop=False)
                currently_playing = {
                    'voice': 1,
                    'neopixel_location': down,
                    'neopixel_color': SAMPLES[sample_num][1],
                    'sample_num': sample_num,
                    'file': f}
        except OSError:
            pass # File not found! skip to next

    time.sleep(0.01)  # a little delay here helps avoid debounce annoyances
    current_press = pressed
This guide was first published on Dec 12, 2018. It was last updated on Dec 12, 2018. This page (Code Walkthrough) was last updated on May 23, 2019.