|
| 1 | +# SPDX-FileCopyrightText: 2022 John Park and Tod Kurt for Adafruit Industries |
| 2 | +# SPDX-License-Identifier: MIT |
| 3 | +# Walkmp3rson digital cassette tape player (ok fine it's just SD cards) |
| 4 | +import time |
| 5 | +import os |
| 6 | +import board |
| 7 | +import busio |
| 8 | +import sdcardio |
| 9 | +import storage |
| 10 | +import audiomixer |
| 11 | +import audiobusio |
| 12 | +import audiomp3 |
| 13 | +from adafruit_neokey.neokey1x4 import NeoKey1x4 |
| 14 | +from adafruit_seesaw import seesaw, rotaryio |
| 15 | +import displayio |
| 16 | +import terminalio |
| 17 | +from adafruit_display_text import label |
| 18 | +from adafruit_st7789 import ST7789 |
| 19 | +from adafruit_progressbar.progressbar import HorizontalProgressBar |
| 20 | +from adafruit_progressbar.verticalprogressbar import VerticalProgressBar |
| 21 | + |
| 22 | + |
| 23 | +displayio.release_displays() |
| 24 | + |
| 25 | +# SPI for TFT display, and SD Card reader on TFT display |
| 26 | +spi = board.SPI() |
| 27 | +# display setup |
| 28 | +tft_cs = board.D6 |
| 29 | +tft_dc = board.D9 |
| 30 | +tft_reset = board.D12 |
| 31 | +display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=tft_reset) |
| 32 | +display = ST7789(display_bus, width=320, height=240, rotation=90) |
| 33 | + |
| 34 | +# SD Card setup |
| 35 | +sd_cs = board.D13 |
| 36 | +sdcard = sdcardio.SDCard(spi, sd_cs) |
| 37 | +vfs = storage.VfsFat(sdcard) |
| 38 | +storage.mount(vfs, "/sd") |
| 39 | + |
| 40 | +# I2C NeoKey setup |
| 41 | +i2c = busio.I2C(board.SCL, board.SDA) |
| 42 | +neokey = NeoKey1x4(i2c, addr=0x30) |
| 43 | +amber = 0x300800 |
| 44 | +red = 0x900000 |
| 45 | +green = 0x009000 |
| 46 | + |
| 47 | +neokey.pixels.fill(amber) |
| 48 | +keys = [ |
| 49 | + (neokey, 0, green), |
| 50 | + (neokey, 1, red), |
| 51 | + (neokey, 2, green), |
| 52 | + (neokey, 3, green), |
| 53 | +] |
| 54 | +# states for key presses |
| 55 | +key_states = [False, False, False, False] |
| 56 | + |
| 57 | +# STEMMA QT Rotary encoder setup |
| 58 | +rotary_seesaw = seesaw.Seesaw(i2c, addr=0x37) # default address is 0x36 |
| 59 | +encoder = rotaryio.IncrementalEncoder(rotary_seesaw) |
| 60 | +last_encoder_pos = 0 |
| 61 | + |
| 62 | +# file system setup |
| 63 | +mp3s = [] |
| 64 | +for filename in os.listdir('/sd'): |
| 65 | + if filename.lower().endswith('.mp3') and not filename.startswith('.'): |
| 66 | + mp3s.append("/sd/"+filename) |
| 67 | + |
| 68 | +mp3s.sort() # sort alphanumerically for mixtape order, e.g., "1_King_of_Rock.mp3" |
| 69 | +for mp3 in mp3s: |
| 70 | + print(mp3) |
| 71 | + |
| 72 | +track_number = 0 |
| 73 | +mp3_filename = mp3s[track_number] |
| 74 | +mp3_bytes = os.stat(mp3_filename)[6] # size in bytes is position 6 |
| 75 | +mp3_file = open(mp3_filename, "rb") |
| 76 | +mp3stream = audiomp3.MP3Decoder(mp3_file) |
| 77 | + |
| 78 | +def tracktext(full_path_name, position): |
| 79 | + return full_path_name.split('_')[position].split('.')[0] |
| 80 | + |
| 81 | +audio = audiobusio.I2SOut(bit_clock=board.D1, word_select=board.D10, data=board.D11) |
| 82 | +mixer = audiomixer.Mixer(voice_count=1, sample_rate=22050, channel_count=1, |
| 83 | + bits_per_sample=16, samples_signed=True) |
| 84 | +mixer.voice[0].level = 0.15 |
| 85 | +audio.play(mixer) |
| 86 | + |
| 87 | +# Colors |
| 88 | +blue_bright = 0x17afcf |
| 89 | +blue_mid = 0x0d6173 |
| 90 | +blue_dark = 0x041f24 |
| 91 | + |
| 92 | +orange_bright = 0xda8c57 |
| 93 | +orange_mid = 0xa46032 |
| 94 | +orange_dark = 0x472a16 |
| 95 | + |
| 96 | +# display |
| 97 | +main_display_group = displayio.Group() # everything goes in main group |
| 98 | +display.show(main_display_group) # show main group (clears screen, too) |
| 99 | + |
| 100 | +# background bitmap w OnDiskBitmap |
| 101 | +tape_bitmap = displayio.OnDiskBitmap(open("mp3_tape.bmp", "rb")) |
| 102 | +tape_tilegrid = displayio.TileGrid(tape_bitmap, pixel_shader=tape_bitmap.pixel_shader) |
| 103 | +main_display_group.append(tape_tilegrid) |
| 104 | + |
| 105 | + |
| 106 | +# song name label |
| 107 | +song_name_text_group = displayio.Group(scale=3, x=90, y=44) # text label goes in this Group |
| 108 | +song_name_text = tracktext(mp3_filename, 2) |
| 109 | +song_name_label = label.Label(terminalio.FONT, text=song_name_text, color=orange_bright) |
| 110 | +song_name_text_group.append(song_name_label) # add the label to the group |
| 111 | +main_display_group.append(song_name_text_group) # add to the parent group |
| 112 | + |
| 113 | +# artist name label |
| 114 | +artist_name_text_group = displayio.Group(scale=2, x=92, y=186) |
| 115 | +artist_name_text = tracktext(mp3_filename, 1) |
| 116 | +artist_name_label = label.Label(terminalio.FONT, text=artist_name_text, color=orange_bright) |
| 117 | +artist_name_text_group.append(artist_name_label) |
| 118 | +main_display_group.append(artist_name_text_group) |
| 119 | + |
| 120 | +# song progress bar |
| 121 | +progress_bar = HorizontalProgressBar( |
| 122 | + (72, 144), |
| 123 | + (174, 12), |
| 124 | + bar_color=blue_bright, |
| 125 | + outline_color=blue_mid, |
| 126 | + fill_color=blue_dark, |
| 127 | +) |
| 128 | +main_display_group.append(progress_bar) |
| 129 | + |
| 130 | +# volume level bar |
| 131 | +volume_bar = VerticalProgressBar( |
| 132 | + (304, 40), |
| 133 | + (8, 170), |
| 134 | + bar_color=orange_bright, |
| 135 | + outline_color=orange_mid, |
| 136 | + fill_color=orange_dark, |
| 137 | +) |
| 138 | +main_display_group.append(volume_bar) |
| 139 | +volume_bar.value = mixer.voice[0].level * 100 |
| 140 | + |
| 141 | +def change_track(tracknum): |
| 142 | + # pylint: disable=global-statement |
| 143 | + global mp3_filename |
| 144 | + mp3_filename = mp3s[tracknum] |
| 145 | + song_name_fc = tracktext(mp3_filename, 2) |
| 146 | + artist_name_fc = tracktext(mp3_filename, 1) |
| 147 | + mp3_file_fc = open(mp3_filename, "rb") |
| 148 | + mp3stream_fc = audiomp3.MP3Decoder(mp3_file_fc) |
| 149 | + mp3_bytes_fc = os.stat(mp3_filename)[6] # size in bytes is position 6 |
| 150 | + # print(mp3_bytes) |
| 151 | + # print("Now playing: '{}'".format(mp3_filename)) |
| 152 | + return (mp3_file_fc, mp3stream_fc, song_name_fc, artist_name_fc, mp3_bytes_fc) |
| 153 | + |
| 154 | +print("Walkmp3rson") |
| 155 | +play_state = False # so we know if we're auto advancing when mixer finishes a song |
| 156 | +last_debug_time = 0 # for timing track position |
| 157 | +reels_anim_frame = 0 |
| 158 | +last_percent_done = 0.01 |
| 159 | + |
| 160 | +while True: |
| 161 | + encoder_pos = -encoder.position |
| 162 | + if encoder_pos != last_encoder_pos: |
| 163 | + encoder_delta = encoder_pos - last_encoder_pos |
| 164 | + volume_adjust = min(max((mixer.voice[0].level + (encoder_delta*0.005)), 0.0), 1.0) |
| 165 | + mixer.voice[0].level = volume_adjust |
| 166 | + |
| 167 | + last_encoder_pos = encoder_pos |
| 168 | + volume_bar.value = mixer.voice[0].level * 100 |
| 169 | + |
| 170 | + if play_state is True: # if not stopped, auto play next mp3_filename |
| 171 | + if time.monotonic() - last_debug_time > 0.2: # so we can check track progress |
| 172 | + last_debug_time = time.monotonic() |
| 173 | + bytes_played = mp3_file.tell() |
| 174 | + percent_done = (bytes_played / mp3_bytes) |
| 175 | + progress_bar.value = min(max(percent_done * 100, 0), 100) |
| 176 | + |
| 177 | + if not mixer.playing: |
| 178 | + print("next song") |
| 179 | + track_number = ((track_number + 1) % len(mp3s)) |
| 180 | + mp3_file, mp3stream, song_name, artist_name, mp3_bytes = change_track(track_number) |
| 181 | + song_name_label.text = song_name |
| 182 | + artist_name_label.text = artist_name |
| 183 | + mixer.voice[0].play(mp3stream, loop=False) |
| 184 | + |
| 185 | + # Use the NeoKeys as transport controls |
| 186 | + for k in range(len(keys)): |
| 187 | + neokey, key_number, color = keys[k] |
| 188 | + if neokey[key_number] and not key_states[key_number]: |
| 189 | + key_states[key_number] = True |
| 190 | + neokey.pixels[key_number] = color |
| 191 | + |
| 192 | + if key_number == 0: # previous track |
| 193 | + track_number = ((track_number - 1) % len(mp3s) ) |
| 194 | + mp3_file, mp3stream, song_name, artist_name, mp3_bytes = change_track(track_number) |
| 195 | + song_name_label.text = song_name |
| 196 | + artist_name_label.text = artist_name |
| 197 | + audio.resume() |
| 198 | + mixer.voice[0].play(mp3stream, loop=False) |
| 199 | + play_state = True |
| 200 | + |
| 201 | + if key_number == 1: # Play/pause |
| 202 | + if play_state: |
| 203 | + audio.pause() |
| 204 | + play_state = False |
| 205 | + else: |
| 206 | + audio.resume() |
| 207 | + play_state = True |
| 208 | + |
| 209 | + if key_number == 2: # Play track from beginning |
| 210 | + audio.resume() |
| 211 | + mixer.voice[0].play(mp3stream, loop=False) |
| 212 | + song_name_label.text = tracktext(mp3_filename, 2) |
| 213 | + artist_name_label.text = tracktext(mp3_filename, 1) |
| 214 | + play_state = True |
| 215 | + |
| 216 | + if key_number == 3: # next track |
| 217 | + track_number = ((track_number + 1) % len(mp3s)) |
| 218 | + mp3_file, mp3stream, song_name, artist_name, mp3_bytes = change_track(track_number) |
| 219 | + song_name_label.text = song_name |
| 220 | + artist_name_label.text = artist_name |
| 221 | + audio.resume() |
| 222 | + mixer.voice[0].play(mp3stream, loop=False) |
| 223 | + play_state = True |
| 224 | + |
| 225 | + if not neokey[key_number] and key_states[key_number]: |
| 226 | + neokey.pixels[key_number] = amber |
| 227 | + key_states[key_number] = False |
0 commit comments