Maker.io main logo

Walkmp3rson Personal MP3 'Tape' Player

2022-11-29 | By Adafruit Industries

License: See Original Project 3D Printing Displays Adafruit Feather

Courtesy of Adafruit

Guide by John Park

Overview

Look the part while you walk around town listening to your ‎favorite mixes. CircuitPython powers this personal music player, ‎with a stylish 3D printed case, TFT display, mech keyswitch ‎controls and more.‎

Pop in a different "mix tape" SD card when you're in the mood ‎for some different tunes.‎

 

 

 

 

 

 

Parts

CAD Files

CAD Parts List

STL files for 3D printing are oriented to print "as-is" on FDM ‎style machines. Parts are designed to 3D print without any ‎support material. Original design source may be downloaded ‎using the links below:‎

  • wp-back-cover

  • wp-button-case

  • wp-button-cover

  • wp-front-case

  • wp-keyswitch-plate

  • wp-switch-holder

  • wp-trs-rotary

Walkmp3rson CAD Files

STLs.zip

Download CAD Source

Build Volume

The parts require a 3D printer with a minimum build volume.‎

  • ‎110mm (X) x 70mm (Y) x 30mm (Z)‎

build_3

Design Source Files

The project assembly was designed in Fusion 360. This can be ‎downloaded in different formats like STEP, STL, and more. ‎Electronic components like Adafruit's boards, displays, ‎connectors, and more can be downloaded from the Adafruit ‎CAD parts GitHub Repo.‎

design_4

Install CircuitPython

CircuitPython is a derivative of MicroPython designed to ‎simplify experimentation and education on low-cost ‎microcontrollers. It makes it easier than ever to get prototyping ‎by requiring no upfront desktop software downloads. Simply ‎copy and edit files on the CIRCUITPY drive to iterate.‎

CircuitPython Quickstart

Follow this step-by-step to quickly get CircuitPython running ‎on your board.‎

Download the latest version of CircuitPython for this board via ‎circuitpython.org

Click the link above to download the latest CircuitPython ‎UF2 file.‎

Save it wherever is convenient for you.‎

download_5

board_6

To enter the bootloader, hold down the BOOT/BOOTSEL ‎button (highlighted in red above), and while continuing to hold ‎it (don't let go!), press and release the reset button (highlighted ‎in blue above). Continue to hold the BOOT/BOOTSEL button ‎until the RPI-RP2 drive appears!‎

If the drive does not appear, release all the buttons, and then ‎repeat the process above.‎

You can also start with your board unplugged from USB, press ‎and hold the BOOTSEL button (highlighted in red above), ‎continue to hold it while plugging it into USB, and wait for the ‎drive to appear before releasing the button.‎

A lot of people end up using charge-only USB cables and it is ‎very frustrating! Make sure you have a USB cable you know is ‎good for data sync.

You will see a new disk drive appear called RPI-RP2.‎‎

‎Drag the adafruit_circuitpython_etc.uf2 file to RPI-RP2.‎

file_7

file_8

The RPI-RP2 drive will disappear, and a new disk drive ‎called CIRCUITPY will appear.‎

That's it, you're done! :)‎

file_9

Safe Mode

You want to edit your code.py or modify the files on ‎your CIRCUITPY drive but find that you can't. Perhaps your ‎board has gotten into a state where CIRCUITPY is read-only. ‎You may have turned off the CIRCUITPY drive altogether. ‎Whatever the reason, safe mode can help.‎

Safe mode in CircuitPython does not run any user code on ‎startup and disables auto-reload. This means a few things. First, ‎safe mode bypasses any code in boot.py (where you can ‎set CIRCUITPY read-only or turn it off completely). Second, it ‎does not run the code in code.py. And finally, it does not ‎automatically soft-reload when data is written to ‎the CIRCUITPY drive.‎

Therefore, whatever you may have done to put your board in a ‎non-interactive state, safe mode gives you the opportunity to ‎correct it without losing all of the data on the CIRCUITPY drive.‎

Entering Safe Mode in CircuitPython 6.x

This section explains entering safe mode on CircuitPython 6.x.‎

mode_10

To enter safe mode when using CircuitPython 6.x, plug in your ‎board or hit reset (highlighted in red above). Immediately after ‎the board starts up or resets, it waits 700ms. On some boards, ‎the onboard status LED (highlighted in green above) will turn ‎solid yellow during this time. If you press reset during that ‎‎700ms, the board will start up in safe mode. It can be difficult to ‎react to the yellow LED, so you may want to think of it simply ‎as a slow double click of the reset button. (Remember, a fast ‎double click of reset enters the bootloader.)‎

Entering Safe Mode in CircuitPython 7.x

This section explains entering safe mode on CircuitPython 7.x.‎

To enter safe mode when using CircuitPython 7.x, plug in your ‎board or hit reset (highlighted in red above). Immediately after ‎the board starts up or resets, it waits 1000ms. On some boards, ‎the onboard status LED (highlighted in green above) will blink ‎yellow during that time. If you press reset during that 1000ms, ‎the board will start up in safe mode. It can be difficult to react ‎to the yellow LED, so you may want to think of it simply as a ‎slow double click of the reset button. (Remember, a fast double ‎click of reset enters the bootloader.)‎

In Safe Mode

Once you've entered safe mode successfully in CircuitPython ‎‎6.x, the LED will pulse yellow.‎

If you successfully enter safe mode on CircuitPython 7.x, the ‎LED will intermittently blink yellow three times.‎

If you connect to the serial console, you'll find the following ‎message.‎

Copy Code
Auto-reload is off.
Running in safe mode! Not running saved code.

CircuitPython is in safe mode because you pressed the reset button during boot. Press again to exit safe mode.

Press any key to enter the REPL. Use CTRL-D to reload.

You can now edit the contents of the CIRCUITPY drive. ‎Remember, your code will not run until you press the reset ‎button, or unplug and plug in your board, to get out of safe ‎mode.‎

Flash Resetting UF2‎

If your board ever gets into a really weird state and doesn't ‎even show up as a disk drive when installing CircuitPython, try ‎loading this 'nuke' UF2 which will do a 'deep clean' on your ‎Flash Memory. You will lose all the files on the board, but at ‎least you'll be able to revive it! After loading this UF2, follow the ‎steps above to re-install CircuitPython.‎

Download flash erasing "nuke" UF2‎

Code the Walkmp3rson

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.‎

Download Project Bundle

Copy Code
# 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)
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.show(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
    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)

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

View on GitHub

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.‎

Download File

Copy Code
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

Setup

Next, the TFT display is set up on the SPI bus.‎

‎Download File

Copy Code
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.‎

Download File

Copy Code
# SD Card setup
sd_cs = board.D13
sdcard = sdcardio.SDCard(spi, sd_cs)
vfs = storage.VfsFat(sdcard)
storage.mount(vfs, "/sd")

NeoKey

The NeoKey is set up on the I2C bus next, including color ‎definitions and key states.‎

‎Download File

Copy Code
# 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]

Rotary Encoder

The rotary encoder is set up as a seesaw device.‎

Download File

Copy Code
# 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

SD Card Filesystem

To read the .mp3 files from the SD card, the filesystem is ‎defined and listed.‎

Download File

Copy Code
# 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’s name Bartlebeats and song name Daisy.‎

Download File

Copy Code
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.‎

‎Download File‎

Copy Code
# 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.‎

Download File

Copy Code
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

Screen Colors

Colors are defined for screen text and graphics.‎

Download File‎

Copy Code
# Colors
blue_bright = 0x17afcf
blue_mid = 0x0d6173
blue_dark = 0x041f24

orange_bright = 0xda8c57
orange_mid = 0xa46032
orange_dark = 0x472a16

Displayio

The displayio group and on disk bitmap are defined next.‎

Download File

Copy Code
display
main_display_group = displayio.Group()  # everything goes in main group
display.show(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)

Text Labels

The text for song and artist is set up here.‎

Download File

Copy Code
# 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.‎

Download File

Copy Code
# 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.‎

‎Download File‎

Copy Code
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)

States

State variables are set, and then the audio mixer is turned on.‎

Download File

Copy Code
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

Download File

Copy Code
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

Build the Walkmp3rson Circuit

diagram_11

diagram_12

Display Connection

Wire the display as shown in the Fritzing diagram and the ‎photos here to the FeatherWing Doubler. ‎

There are many ways to do this -- I used right angled headers ‎and Dupont connector silicone jumper wires.‎

display_13

display_14

Amp Prep

Solder on header pins to the amp board so you can mount it to ‎the Doubler.‎

I chose to solder a small socket header to connect the amp ‎breakout to the headphone jack using jumper cables later.‎

amp_15

Amp Connections

Solder the amp breakout to the long free row of the doubler as ‎shown.‎

Then, use short wires to connect the legs to their respective ‎pins on the Feather via the Doubler connections.‎

connections_16

connections_17

Amp Gain Resistor

Solder a 100KΩ resistor from the Gain pin to the Vin pin. This ‎sets the amplifier gain to 3dB, which works well for the ‎headphone output.‎

output_18

Headphone Out

To keep the project simple, we're using a single mono amplifier. ‎Since most headphones have a stereo TRS 3.5mm plug, you'll ‎run the audio signal to both the tip and ring connectors.‎

Run a single wire to the ground connector.‎

I used Dupont connector cables for this and dressed them with ‎heat shrink tubing.‎

headphone_19

headphone_20

cables_21

On/Off Switch

Solder a wire from the Doubler/Feather Enable pin to one side ‎of a SPDT switch, and another wire from GND to the center ‎position of the switch.‎

switch_22

switch_23

switch_24

NeoKey 1x4 and QT Rotary Encoder

Use STEMMA QT cables to connect the Feather to the NeoKeys ‎and rotary encoder breakout.‎

Plug in the battery as well -- since the amp Vin is connected to ‎the Feather's Bat output, you'll need the battery connected ‎even when plugged into USB for coding.‎

neo_25

neo_26

neo_27

Assemble the Walkmp3rson

assemble_28

Case Parts

Print the case parts in 80s-tastic colors (or any colors you like). ‎The files and notes for printing can be found on the CAD ‎Files page.‎

Then, screw in the nylon standoffs for the Doubler mounting.‎

Display Mount

Mount the display as shown, using M2.5 hardware.‎

mount_29

Feather Doubler Mount

Connect the Doubler to the standoffs as shown.‎

feather_30

feather_31

Switch Mount

Mount the switch into the switch housing thingamajob.‎

housing_32

Button Mount

Insert the keyswitches into the mounting plate.‎

Then, insert the assembly into the case top.‎

Carefully press the switches into the NeoKey PCB.‎

Once assembled, add your keycaps.‎

button_33

button_34

button_35

button_36

button_37

Rotary Mount

Fit the rotary encoder breakout into the case side, then place ‎the washer over the shaft. Screw on the nut to secure it in place.‎

Add a knob to the shaft as shown.‎

rotary_38

rotary_39

rotary_40

SD Card Extender

Insert the prepared SD card mix tape into the SD card extender, ‎then insert it all into the display as shown.‎

card_42

card_41

On/Off Mount

Use M2.5 hardware to fasten the switch mount to the case as ‎shown.‎

on_43

on_44

STEMMA QT Connections

Use 100mm STEMMA QT cables to connect the Feather RP2040 ‎to the rotary encoder and NeoKey breakout.‎

Insert the panel mount headphone jack and screw on the ‎retaining nut.‎

stemma_45

stemma_46

Final Assembly

Press fit the front and back together and use the side piece to ‎hold them together.‎

Then, press the top piece into place.‎

final_48

final_47

final_49

制造商零件编号 4884
ADAFRUIT FEATHER RP2040
Adafruit Industries LLC
制造商零件编号 4311
GRAPHIC DISPLAY TFT RGB 2"
Adafruit Industries LLC
制造商零件编号 4980
NEOKEY 1X4 QT I2C - FOUR MECHANI
Adafruit Industries LLC
制造商零件编号 4955
KAILH MECH KEY SW 1=PACK OF 10
Adafruit Industries LLC
制造商零件编号 5174
CYAN MA KEYCAPS - 5 PACK
Adafruit Industries LLC
制造商零件编号 4991
ADAFRUIT I2C QT ROTARY ENCODER W
Adafruit Industries LLC
制造商零件编号 2890
FEATHERWING DOUBLER - PROTOTYPIN
Adafruit Industries LLC
制造商零件编号 3006
EVAL BOARD FOR MAX98357A
Adafruit Industries LLC
制造商零件编号 4210
JST SH 4-PIN CABLE - QWIIC COMPA
Adafruit Industries LLC
制造商零件编号 4395
MICRO SD CARD PCB EXTENDER
Adafruit Industries LLC
制造商零件编号 2940
SHORT FEATHER HEADERS KIT - 12-P
Adafruit Industries LLC
制造商零件编号 3002
SHORT FEATHER MALE HEADERS - 12-
Adafruit Industries LLC
制造商零件编号 5533
ORANGE MICRO POTENTIOMETER KNOB
Adafruit Industries LLC
制造商零件编号 5250
MEM CARD MICROSD 128MB CLASS 4
Adafruit Industries LLC
制造商零件编号 3111
HOOK-UP 22AWG STRAND - 6 X 25FT
Adafruit Industries LLC
制造商零件编号 4474
CABLE A PLUG TO C PLUG 3'
Adafruit Industries LLC
Add all DigiKey Parts to Cart
TechForum

Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.

Visit TechForum