We'll be using CircuitPython for this project. Are you new to using CircuitPython? No worries, there is a full getting started guide here.

Adafruit suggests using the Mu editor to edit your code and have an interactive REPL in CircuitPython. You can learn about Mu and its installation in this tutorial.

There's a guide to get you up and running with Circuit Python specifically for the NeoTrellis M4. You should read it before starting to get the most recent CircuitPython build for the NeoTrellis M4 installed and running along with the required libraries.

Be sure to use at least CircuitPython 4.0 alpha 3 for the NeoTrellis M4, as it uses the latest audioio capabilities.

Navigating the NeoTrellis

To get your NeoTrellis M4 set up to run this project's code, first follow these steps:

1) Update the bootloader for NeoTrellis from the NeoTrellis M4 guide

2) Install the latest CircuitPython for NeoTrellis from the NeoTrellis M4 guide

For this project you will need the following libraries:

  • adafruit_trellism4.mpy
  • neopixel.mpy
  • adafruit_matrixkeypad.mpy

Download Code

Time to install the software, here's the code.py listing, click on the Download Project Bundle link in the top left to grab all the code.

# SPDX-FileCopyrightText: 2018 Dave Astels for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""
Tabletop RPG soundboard for the NeoTrellisM4

Adafruit invests time and resources providing this open source code.
Please support Adafruit and open source hardware by purchasing
products from Adafruit!

Written by Dave Astels for Adafruit Industries
Copyright (c) 2018 Adafruit Industries
Licensed under the MIT license.

All text above must be included in any redistribution.
"""

# pylint: disable=wildcard-import,unused-wildcard-import,eval-used

import time
import board
import audioio
import audiocore
import audiomixer
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 = []
BLACK = 0x000000


# load the sound & color specifications
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())))


# Parse the first file to figure out what format its in
channel_count = None
bits_per_sample = None
sample_rate = None
with open(SAMPLE_FOLDER+SAMPLES[0][0], 'rb') as f:
    wav = audiocore.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!')

mixer = audiomixer.Mixer(voice_count=2,
                      sample_rate=sample_rate,
                      channel_count=channel_count,
                      bits_per_sample=bits_per_sample,
                      samples_signed=True)
audio.play(mixer)

# Clear all pixels
trellis.pixels.fill(0)

# Light up button with a valid sound file attached
for i, v in enumerate(SAMPLES):
    filename = SAMPLE_FOLDER+v[0]
    try:
        with open(filename, 'rb') as f:
            wav = audiocore.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


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


current_press = set()
current_background = {'voice' : None}
currently_playing = {'voice' : None}
while True:
    pressed = set(trellis.pressed_keys)
    just_pressed = pressed - current_press
    # just_released = current_press - pressed

    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 = audiocore.WaveFile(f)

            if down[1] == 0:              # background loop?
                if current_background['voice'] != None:
                    print('Interrupt')
                    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:
                    print('Interrupt')
                    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

    # # check if any samples are done
    # # this currently doesn't work with the mixer until it supports per voice "is_playing" checking
    # if not audio.playing and currently_playing['voice'] != None:
    #     stop_playing_sample(currently_playing)

    time.sleep(0.01)  # a little delay here helps avoid debounce annoyances
    current_press = pressed

Use

The directory/file arrangement on CIRCUITPY after unzipping the the project is shown below. 

The code.py CircuitPython code should run as soon as you copy the file onto the NeoTrellis CIRCUITPY drive. Be sure you copy all the files shown above to your CIRCUITPY drive.

Now you can start pressing buttons for your sound effects!

If some sounds work but not others, you'll need to edit the soundboard.txt file as explained on the previous page to ensure the file names are correct and you get the color you want for the button.

If you believe you have the soundboard.txt file correct and you do not hear the sound:

  • Check your cabling to ensure you have powered amplified speakers and that the are on and volume at a good level.
  • Be sure all of your sounds have been converted to 22,050 KHz, 16-bit PCM WAV files. Use all Mono or Stereo files, don't mix Mono and Stereo. This guide will help you with the conversions. MP3 files cannot be played in this project, sorry, they'll need conversion to WAV.

This guide was first published on Dec 12, 2018. It was last updated on Mar 28, 2024.

This page (Code and Use) was last updated on Mar 27, 2024.

Text editor powered by tinymce.