Text Editor
Adafruit recommends using the Mu editor for editing your CircuitPython code. You can get more info in this guide.
Alternatively, you can use any text editor that saves simple text files.
Download the Project Bundle
Your project will use a specific set of CircuitPython libraries, sample .mp3s, graphic .bmp, and the code.py file. To get everything you need, click on the Download Project Bundle link below, and uncompress the .zip file.
Drag the contents of the uncompressed bundle directory onto your board's CIRCUITPY drive, replacing any existing files or directories with the same names, and adding any new ones that are necessary.
Audio Files
Copy the .mp3 files from the bundle onto your SD card using a USB SD card reader.
To make your own files, follow the info in this guide on converting audio files to mono .mp3 files.
# SPDX-FileCopyrightText: 2022 John Park and Tod Kurt for Adafruit Industries # SPDX-License-Identifier: MIT '''Walkmp3rson digital cassette tape player (ok fine it's just SD cards)''' import time import os import board import busio import sdcardio import storage import audiomixer import audiobusio import audiomp3 from adafruit_neokey.neokey1x4 import NeoKey1x4 from adafruit_seesaw import seesaw, rotaryio import displayio import terminalio from adafruit_display_text import label from adafruit_st7789 import ST7789 from adafruit_progressbar.progressbar import HorizontalProgressBar from adafruit_progressbar.verticalprogressbar import VerticalProgressBar displayio.release_displays() # SPI for TFT display, and SD Card reader on TFT display spi = board.SPI() # display setup tft_cs = board.D6 tft_dc = board.D9 tft_reset = board.D12 display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=tft_reset) display = ST7789(display_bus, width=320, height=240, rotation=90) # SD Card setup sd_cs = board.D13 sdcard = sdcardio.SDCard(spi, sd_cs) vfs = storage.VfsFat(sdcard) storage.mount(vfs, "/sd") # I2C NeoKey setup i2c = busio.I2C(board.SCL, board.SDA) neokey = NeoKey1x4(i2c, addr=0x30) amber = 0x300800 red = 0x900000 green = 0x009000 neokey.pixels.fill(amber) keys = [ (neokey, 0, green), (neokey, 1, red), (neokey, 2, green), (neokey, 3, green), ] # states for key presses key_states = [False, False, False, False] # STEMMA QT Rotary encoder setup rotary_seesaw = seesaw.Seesaw(i2c, addr=0x36) # default address is 0x36 encoder = rotaryio.IncrementalEncoder(rotary_seesaw) last_encoder_pos = 0 # file system setup mp3s = [] for filename in os.listdir('/sd'): if filename.lower().endswith('.mp3') and not filename.startswith('.'): mp3s.append("/sd/"+filename) mp3s.sort() # sort alphanumerically for mixtape order, e.g., "1_King_of_Rock.mp3" for mp3 in mp3s: print(mp3) track_number = 0 mp3_filename = mp3s[track_number] mp3_bytes = os.stat(mp3_filename)[6] # size in bytes is position 6 mp3_file = open(mp3_filename, "rb") mp3stream = audiomp3.MP3Decoder(mp3_file) def tracktext(full_path_name, position): return full_path_name.split('_')[position].split('.')[0] # LRC is word_select, BCLK is bit_clock, DIN is data_pin. # Feather RP2040 audio = audiobusio.I2SOut(bit_clock=board.D24, word_select=board.D25, data=board.A3) # Feather M4 # audio = audiobusio.I2SOut(bit_clock=board.D1, word_select=board.D10, data=board.D11) mixer = audiomixer.Mixer(voice_count=1, sample_rate=22050, channel_count=1, bits_per_sample=16, samples_signed=True, buffer_size=32768) mixer.voice[0].level = 0.15 # Colors blue_bright = 0x17afcf blue_mid = 0x0d6173 blue_dark = 0x041f24 orange_bright = 0xda8c57 orange_mid = 0xa46032 orange_dark = 0x472a16 # display main_display_group = displayio.Group() # everything goes in main group display.root_group = main_display_group # show main group (clears screen, too) # background bitmap w OnDiskBitmap tape_bitmap = displayio.OnDiskBitmap(open("mp3_tape.bmp", "rb")) tape_tilegrid = displayio.TileGrid(tape_bitmap, pixel_shader=tape_bitmap.pixel_shader) main_display_group.append(tape_tilegrid) # song name label song_name_text_group = displayio.Group(scale=3, x=90, y=44) # text label goes in this Group song_name_text = tracktext(mp3_filename, 2) song_name_label = label.Label(terminalio.FONT, text=song_name_text, color=orange_bright) song_name_text_group.append(song_name_label) # add the label to the group main_display_group.append(song_name_text_group) # add to the parent group # artist name label artist_name_text_group = displayio.Group(scale=2, x=92, y=186) artist_name_text = tracktext(mp3_filename, 1) artist_name_label = label.Label(terminalio.FONT, text=artist_name_text, color=orange_bright) artist_name_text_group.append(artist_name_label) main_display_group.append(artist_name_text_group) # song progress bar progress_bar = HorizontalProgressBar( (72, 144), (174, 12), bar_color=blue_bright, outline_color=blue_mid, fill_color=blue_dark, ) main_display_group.append(progress_bar) # volume level bar volume_bar = VerticalProgressBar( (304, 40), (8, 170), bar_color=orange_bright, outline_color=orange_mid, fill_color=orange_dark, ) main_display_group.append(volume_bar) volume_bar.value = mixer.voice[0].level * 100 def change_track(tracknum): # pylint: disable=global-statement global mp3_filename # pylint: disable=global-statement global mp3stream mp3_filename = mp3s[tracknum] song_name_fc = tracktext(mp3_filename, 2) artist_name_fc = tracktext(mp3_filename, 1) mp3_file_fc = open(mp3_filename, "rb") mp3stream.file = mp3_file_fc mp3stream_fc = mp3stream mp3_bytes_fc = os.stat(mp3_filename)[6] return (mp3_file_fc, mp3stream_fc, song_name_fc, artist_name_fc, mp3_bytes_fc) print("Walkmp3rson") play_state = False # so we know if we're auto advancing when mixer finishes a song last_debug_time = 0 # for timing track position reels_anim_frame = 0 last_percent_done = 0.01 audio.play(mixer) while True: encoder_pos = -encoder.position if encoder_pos != last_encoder_pos: encoder_delta = encoder_pos - last_encoder_pos volume_adjust = min(max((mixer.voice[0].level + (encoder_delta*0.005)), 0.0), 1.0) mixer.voice[0].level = volume_adjust last_encoder_pos = encoder_pos volume_bar.value = mixer.voice[0].level * 100 if play_state is True: # if not stopped, auto play next song if time.monotonic() - last_debug_time > 0.2: # so we can check track progress last_debug_time = time.monotonic() bytes_played = mp3_file.tell() percent_done = (bytes_played / mp3_bytes) progress_bar.value = min(max(percent_done * 100, 0), 100) if not mixer.playing: print("next song") audio.pause() track_number = ((track_number + 1) % len(mp3s)) mp3_file, mp3stream, song_name, artist_name, mp3_bytes = change_track(track_number) song_name_label.text = song_name artist_name_label.text = artist_name mixer.voice[0].play(mp3stream, loop=False) time.sleep(.1) audio.resume() # Use the NeoKeys as transport controls for k in range(len(keys)): neokey, key_number, color = keys[k] if neokey[key_number] and not key_states[key_number]: key_states[key_number] = True neokey.pixels[key_number] = color if key_number == 0: # previous track audio.pause() track_number = ((track_number - 1) % len(mp3s) ) mp3_file, mp3stream, song_name, artist_name, mp3_bytes = change_track(track_number) song_name_label.text = song_name artist_name_label.text = artist_name mixer.voice[0].play(mp3stream, loop=False) play_state = True time.sleep(.1) audio.resume() if key_number == 1: # Play/pause if play_state: audio.pause() play_state = False else: audio.resume() play_state = True if key_number == 2: # Play track from beginning audio.pause() mixer.voice[0].play(mp3stream, loop=False) song_name_label.text = tracktext(mp3_filename, 2) artist_name_label.text = tracktext(mp3_filename, 1) play_state = True time.sleep(.1) audio.resume() if key_number == 3: # next track audio.pause() track_number = ((track_number + 1) % len(mp3s)) mp3_file, mp3stream, song_name, artist_name, mp3_bytes = change_track(track_number) song_name_label.text = song_name artist_name_label.text = artist_name mixer.voice[0].play(mp3stream, loop=False) play_state = True time.sleep(.1) audio.resume() if not neokey[key_number] and key_states[key_number]: neokey.pixels[key_number] = amber key_states[key_number] = False
How it Works
First, there are a dozen or so libraries to install! These help us use the SD card, TFT display, audio mixer and mp3 decoder, NeoKeys, rotary encoder, and more.
import time import os import board import busio import sdcardio import storage import audiomixer import audiobusio import audiomp3 from adafruit_neokey.neokey1x4 import NeoKey1x4 from adafruit_seesaw import seesaw, rotaryio import displayio import terminalio from adafruit_display_text import label from adafruit_st7789 import ST7789 from adafruit_progressbar.progressbar import HorizontalProgressBar from adafruit_progressbar.verticalprogressbar import VerticalProgressBar
displayio.release_displays() # SPI for TFT display, and SD Card reader on TFT display spi = board.SPI() # display setup tft_cs = board.D6 tft_dc = board.D9 tft_reset = board.D12 display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=tft_reset) display = ST7789(display_bus, width=320, height=240, rotation=90)
Then, the SD card reader is set up, also on SPI.
# SD Card setup sd_cs = board.D13 sdcard = sdcardio.SDCard(spi, sd_cs) vfs = storage.VfsFat(sdcard) storage.mount(vfs, "/sd")
# I2C NeoKey setup i2c = busio.I2C(board.SCL, board.SDA) neokey = NeoKey1x4(i2c, addr=0x30) amber = 0x300800 red = 0x900000 green = 0x009000 neokey.pixels.fill(amber) keys = [ (neokey, 0, green), (neokey, 1, red), (neokey, 2, green), (neokey, 3, green), ] # states for key presses key_states = [False, False, False, False]
# STEMMA QT Rotary encoder setup rotary_seesaw = seesaw.Seesaw(i2c, addr=0x36) # default address is 0x36 encoder = rotaryio.IncrementalEncoder(rotary_seesaw) last_encoder_pos = 0
# file system setup mp3s = [] for filename in os.listdir('/sd'): if filename.lower().endswith('.mp3') and not filename.startswith('.'): mp3s.append("/sd/"+filename) mp3s.sort() # sort alphanumerically for mixtape order, e.g., "1_King_of_Rock.mp3" for mp3 in mp3s: print(mp3) track_number = 0 mp3_filename = mp3s[track_number] mp3_bytes = os.stat(mp3_filename)[6] # size in bytes is position 6 mp3_file = open(mp3_filename, "rb") mp3stream = audiomp3.MP3Decoder(mp3_file)
Tracktext Function
This function is used to parse the track names for display, removing the track number from the beginning, and returning the Artist Name and Song Name for display. This is the naming convention:
number_artist_song.mp3
For example:
03_Bartlebeats_Daisy.mp3
That will be the third song on the "mix tape" SD card, with the artist name Bartlebeats and song name Daisy.
def tracktext(full_path_name, position): return full_path_name.split('_')[position].split('.')[0]
Audio
The audio is set up as an I2S audio output on the audiobus with pins defined for word select, bit clock, and data.
# LRC is word_select, BCLK is bit_clock, DIN is data_pin. # Feather RP2040 audio = audiobusio.I2SOut(bit_clock=board.D24, word_select=board.D25, data=board.A3) # Feather M4 # audio = audiobusio.I2SOut(bit_clock=board.D1, word_select=board.D10, data=board.D11)
Mixer
The mixer object is created, with the specific settings we're using for the .mp3 files and an initial volume level.
mixer = audiomixer.Mixer(voice_count=1, sample_rate=22050, channel_count=1, bits_per_sample=16, samples_signed=True) mixer.voice[0].level = 0.15
# Colors blue_bright = 0x17afcf blue_mid = 0x0d6173 blue_dark = 0x041f24 orange_bright = 0xda8c57 orange_mid = 0xa46032 orange_dark = 0x472a16
display main_display_group = displayio.Group() # everything goes in main group display.root_group = main_display_group # show main group (clears screen, too) # background bitmap w OnDiskBitmap tape_bitmap = displayio.OnDiskBitmap(open("mp3_tape.bmp", "rb")) tape_tilegrid = displayio.TileGrid(tape_bitmap, pixel_shader=tape_bitmap.pixel_shader) main_display_group.append(tape_tilegrid)
# song name label song_name_text_group = displayio.Group(scale=3, x=90, y=44) # text label goes in this Group song_name_text = tracktext(mp3_filename, 2) song_name_label = label.Label(terminalio.FONT, text=song_name_text, color=orange_bright) song_name_text_group.append(song_name_label) # add the label to the group main_display_group.append(song_name_text_group) # add to the parent group # artist name label artist_name_text_group = displayio.Group(scale=2, x=92, y=186) artist_name_text = tracktext(mp3_filename, 1) artist_name_label = label.Label(terminalio.FONT, text=artist_name_text, color=orange_bright) artist_name_text_group.append(artist_name_label) main_display_group.append(artist_name_text_group)
Progress Bars
We're using the horizontal progress bar to visualize song length and a vertical progress bar for volume.
# song progress bar progress_bar = HorizontalProgressBar( (72, 144), (174, 12), bar_color=blue_bright, outline_color=blue_mid, fill_color=blue_dark, ) main_display_group.append(progress_bar) # volume level bar volume_bar = VerticalProgressBar( (304, 40), (8, 170), bar_color=orange_bright, outline_color=orange_mid, fill_color=orange_dark, ) main_display_group.append(volume_bar) volume_bar.value = mixer.voice[0].level * 100
change_track()
Function
This function is called whenever a track change happens. This can be when the previous or next song buttons are pressed, or when one song ends and the next needs to begin.
def change_track(tracknum): global mp3_filename mp3_filename = mp3s[tracknum] song_name_fc = tracktext(mp3_filename, 2) artist_name_fc = tracktext(mp3_filename, 1) mp3_file_fc = open(mp3_filename, "rb") mp3stream_fc = audiomp3.MP3Decoder(mp3_file_fc) mp3_bytes_fc = os.stat(mp3_filename)[6] # size in bytes is position 6 return (mp3_file_fc, mp3stream_fc, song_name_fc, artist_name_fc, mp3_bytes_fc)
play_state = False # so we know if we're auto advancing when mixer finishes a song last_debug_time = 0 # for timing track position last_percent_done = 0.01 audio.play(mixer)
Main Loop
The main loop of the program does the following:
- Checks for encoder input to change volume
- Checks the NeoKeys for input
- Plays the current song until it is paused, finishes, is restarted, or next/previous buttons are pressed
while True: encoder_pos = -encoder.position if encoder_pos != last_encoder_pos: encoder_delta = encoder_pos - last_encoder_pos volume_adjust = min(max((mixer.voice[0].level + (encoder_delta*0.005)), 0.0), 1.0) mixer.voice[0].level = volume_adjust last_encoder_pos = encoder_pos volume_bar.value = mixer.voice[0].level * 100 if play_state is True: # if not stopped, auto play next song if time.monotonic() - last_debug_time > 0.2: # so we can check track progress last_debug_time = time.monotonic() bytes_played = mp3_file.tell() percent_done = (bytes_played / mp3_bytes) progress_bar.value = min(max(percent_done * 100, 0), 100) if not mixer.playing: print("next song") audio.pause() track_number = ((track_number + 1) % len(mp3s)) mp3_file, mp3stream, song_name, artist_name, mp3_bytes = change_track(track_number) song_name_label.text = song_name artist_name_label.text = artist_name mixer.voice[0].play(mp3stream, loop=False) time.sleep(.1) audio.resume() # Use the NeoKeys as transport controls for k in range(len(keys)): neokey, key_number, color = keys[k] if neokey[key_number] and not key_states[key_number]: key_states[key_number] = True neokey.pixels[key_number] = color if key_number == 0: # previous track audio.pause() track_number = ((track_number - 1) % len(mp3s) ) mp3_file, mp3stream, song_name, artist_name, mp3_bytes = change_track(track_number) song_name_label.text = song_name artist_name_label.text = artist_name mixer.voice[0].play(mp3stream, loop=False) play_state = True time.sleep(.1) audio.resume() if key_number == 1: # Play/pause if play_state: audio.pause() play_state = False else: audio.resume() play_state = True if key_number == 2: # Play track from beginning audio.pause() mixer.voice[0].play(mp3stream, loop=False) song_name_label.text = tracktext(mp3_filename, 2) artist_name_label.text = tracktext(mp3_filename, 1) play_state = True time.sleep(.1) audio.resume() if key_number == 3: # next track audio.pause() track_number = ((track_number + 1) % len(mp3s)) mp3_file, mp3stream, song_name, artist_name, mp3_bytes = change_track(track_number) song_name_label.text = song_name artist_name_label.text = artist_name mixer.voice[0].play(mp3stream, loop=False) play_state = True time.sleep(.1) audio.resume() if not neokey[key_number] and key_states[key_number]: neokey.pixels[key_number] = amber key_states[key_number] = False
Text editor powered by tinymce.