236 lines
8.1 KiB
Python
236 lines
8.1 KiB
Python
# 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.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
|