# This is a CircuitPython implementation of a simple "wav trigger". # Tested with the WeAct Studio RP2040 board with 16MB of flash memory # At least 4MB of flash memory is recommended to allow for larger samples. # Monophonic .wav files with 16 bits per sample and 44100 samples per second are used. # They should be placed in the /samples directory # The following libraries are required in the /lib directory # adafruit_bitmap_font # adafruit_display_text # adafruit_displayio # The following font is required in the /fonts directory # Helvetica-Bold-16.bdf import adafruit_74hc595 from adafruit_bitmap_font import bitmap_font from adafruit_display_text import label import adafruit_displayio_ssd1306 import audiobusio import audiocore import audiomixer import board import busio import digitalio import displayio from microcontroller import nvm import os import rotaryio import terminalio from time import monotonic from time import sleep # Initialise shift register for button LEDs latch_pin = digitalio.DigitalInOut(board.GP13) spi = busio.SPI(board.GP14, MOSI=board.GP15) sr = adafruit_74hc595.ShiftRegister74HC595(spi, latch_pin) leds = [sr.get_pin(n) for n in range(8)] for n in range(8) : leds[n].value = False # Prepare the display # Free any previously used display displayio.release_displays() # We will use I2C to control the screen i2c = busio.I2C (scl=board.GP27, sda=board.GP26) display_bus = displayio.I2CDisplay(i2c, device_address=0x3C) # Define the dimentions of the physical screen WIDTH = 128 HEIGHT = 64 # Change to 64 if needed # Define the type and size of the display display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=WIDTH, height=HEIGHT, rotation=180) # Create a group of graphic objects group = displayio.Group() # Cause the display to actually show the group display.show(group) # Define the bimap width, height, pixel depth color_bitmap = displayio.Bitmap(WIDTH, HEIGHT, 1) # The bitmap indexes into a palette. # Create the palette and specify its number of colors. color_palette = displayio.Palette(1) # Define the color(s) in the palette color_palette[0] = 0xFFFFFF # White # Open bitmap font font = bitmap_font.load_font("/fonts/Helvetica-Bold-16.bdf") # Create a centered text object #text = "01234567890123" text = " wav trigger" #text = "012345678901234567890" #text = " Pico wav trigger " #text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF, x=0, y=HEIGHT // 2 - 1) text_area = label.Label(font, text=text, color=0xFFFFFF, x=0, y=HEIGHT // 2 - 1) group.append(text_area) max_text_length = 13 clear_delay = 7 # Time to wait before blanking the display clear_time = monotonic() + clear_delay # A press of the on board key safely aborts the program, closing all opened files. key = digitalio.DigitalInOut(board.GP23) key.switch_to_input(pull=digitalio.Pull.DOWN) # The onboard LED toggles on each trigger and slowly flashes afyter the program has been aborted. led = digitalio.DigitalInOut(board.LED) led.switch_to_output() led.value = False # Define the rotary encoder enc_button = digitalio.DigitalInOut(board.GP19) # Encoder button switch enc_button.switch_to_input(pull=digitalio.Pull.DOWN) enc = rotaryio.IncrementalEncoder(board.GP20, board.GP21, 4) enc.position = 0 # Instantiate I2S audio interface # BCK LRCK DIN i2s = audiobusio.I2SOut(board.GP16, board.GP17, board.GP18) # Create a 6 channel mixer, specifying the sample size and sampling rate. # Larger buffer_size reduces gliches but increases latency mixer = audiomixer.Mixer(buffer_size=512, voice_count=6, sample_rate=44100, channel_count=1, bits_per_sample=16, samples_signed=True) # Triggers are connect to the following input pins. # These can be played when in wav trigger mode. trigger_0 = digitalio.DigitalInOut(board.GP0) trigger_1 = digitalio.DigitalInOut(board.GP1) trigger_2 = digitalio.DigitalInOut(board.GP2) trigger_3 = digitalio.DigitalInOut(board.GP3) trigger_4 = digitalio.DigitalInOut(board.GP4) trigger_5 = digitalio.DigitalInOut(board.GP5) # Here we define the active state of the triggers active_high = True if active_high : # Active high inputs are pulled down internally to avoid false triggering when nothing is connected. trigger_0.switch_to_input(pull=digitalio.Pull.DOWN) trigger_1.switch_to_input(pull=digitalio.Pull.DOWN) trigger_2.switch_to_input(pull=digitalio.Pull.DOWN) trigger_3.switch_to_input(pull=digitalio.Pull.DOWN) trigger_4.switch_to_input(pull=digitalio.Pull.DOWN) trigger_5.switch_to_input(pull=digitalio.Pull.DOWN) else : # Active low inputs are pulled up internally to avoid false triggering when nothing is connected. trigger_0.switch_to_input(pull=digitalio.Pull.UP) trigger_1.switch_to_input(pull=digitalio.Pull.UP) trigger_2.switch_to_input(pull=digitalio.Pull.UP) trigger_3.switch_to_input(pull=digitalio.Pull.UP) trigger_4.switch_to_input(pull=digitalio.Pull.UP) trigger_5.switch_to_input(pull=digitalio.Pull.UP) triggers = [trigger_0, trigger_1, trigger_2, trigger_3, trigger_4, trigger_5 ] old_trigger_values = [ active_high, active_high, active_high, active_high, active_high, active_high ] # Read the name of the samples from the flash memory drive. all_filenames = os.listdir("/samples") all_filenames.sort() #print(all_filenames) number_of_files = len(all_filenames) sleep(1) # Show all file names on display as info but also to speed up display during editing. for text in all_filenames : text = text[0:-4] if len(text) > max_text_length : text = text[0:max_text_length-1] text_area.text = text # Blank screen text_area.text = "" sleep(.25) # Initialize the samples list from non-volatile memory samples = [] files = [] wavs = [] for sample in range(6) : print("nvm["+str(sample)+"] = "+ str(nvm[sample]) ) samples.append( nvm[sample] ) if samples[sample] > number_of_files : samples[sample] = 0 filename = all_filenames[samples[sample]] print( filename ) f = open("/samples/"+filename, "rb") wav = audiocore.WaveFile(f) files.append( f ) wavs.append( wav ) # Have AudioOut play our Mixer source i2s.play(mixer) last_active = 0 while True : any_trigger = False input_changed = False for i in range(6) : trigger_value = ( triggers[i].value == active_high ) # trigger_value is high iff trigger[i].value is active any_trigger = any_trigger or trigger_value if trigger_value != old_trigger_values[i] : if trigger_value : # New active trigger mixer.stop_voice(i) mixer.voice[i].play(wavs[i]) leds[last_active].value = False last_active = i leds[last_active].value = True old_trigger_values[i] = trigger_value input_changed = True led.value = any_trigger if enc.position != 0 : # Change sample for last active channel # Mute sound to avoid glitch i2s.stop() # Stop currently playing sample mixer.stop_voice(last_active) # Close old wav file files[last_active].close() # Update wav file selection. samples[last_active] = ( samples[last_active] + enc.position ) % number_of_files enc.position = 0 # Select new wav file. filename = all_filenames[samples[last_active]] # Update display text text = filename[0:-4] if len(text) > max_text_length : text = text[0:max_text_length-1] # display.autorefresh = False text_area.text = text # display.autorefresh = True clear_time = monotonic() + clear_delay # Open new wav file f = open("/samples/"+filename, "rb") files[last_active]= f # Prepare to play new wav file wav = audiocore.WaveFile(f) wavs[last_active]= wav # Wait for screen to finish updating. sleep(.1) # Only play wav once the screen is updated to avoid parasitic noise # from bug in screen or mixer library. i2s.play(mixer) mixer.voice[last_active].play(wavs[last_active]) if enc_button.value : # Save selected sample for next power up nvm[last_active] = samples[last_active] text_area.text = "Saved sample" + str(last_active) clear_time = monotonic() + clear_delay sleep(.25) if input_changed : sleep(.003) # Debounce switches to avoid retriggering if key.value : # Abort program. break if monotonic() > clear_time and text_area.text != "" : text_area.text = "" if key.value : # Kill all channels for i in range(6) : mixer.stop_voice(i) # Stop I2S interface i2s.stop() # Close open files for i in range(6) : files[i].close() print("Aborted, press CTRL-C to stop then any key to prevent restarting.") # Wait in a safe state indicated by a slow blinking LED while True : led.value = not led.value sleep(0.5)