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.
import time import board import audioio import audiocore 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).
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.
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!')
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.
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
.
trellis.pixels.fill(0) 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
Whenever a sound is played, it's details are captured in a structure. That structure is used in the stop_playing_sample
function:
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.
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.
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.
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: 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
Page last edited March 08, 2024
Text editor powered by tinymce.