Rename Player sketch file

This commit is contained in:
seo
2026-06-07 01:15:31 +09:00
parent a0f40175d4
commit 2076f83a99
+463
View File
@@ -0,0 +1,463 @@
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <DFRobotDFPlayerMini.h>
#include <SD.h>
// --- 핀 설정 ---
#define TFT_DC 28
#define TFT_RST 27
#define TFT_CS 22
#define SD_CS 5
#define ENCODER_S1 14
#define ENCODER_S2 13
#define ENCODER_KEY 15
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
DFRobotDFPlayerMini myDFPlayer;
struct Song {
String title;
String artist;
long totalSeconds;
};
Song playlist[100];
int totalTracks = 0;
int currentTrack = 1;
int volume = 15;
bool isVolumeMode = false;
bool isPaused = true;
bool isShuffle = false; // 셔플 상태 변수
float artistScrollX = 10;
int artistPixelWidth = 0;
// --- 엔코더 로직 ---
volatile int encoderPos = 0;
int lastReportedPos = 0;
static uint8_t encoderState = 0;
const int8_t KNOB_DIR[] = {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0};
void readEncoder() {
encoderState <<= 2;
encoderState |= (digitalRead(ENCODER_S1) << 1) | digitalRead(ENCODER_S2);
encoderPos += KNOB_DIR[encoderState & 0x0F];
}
unsigned long buttonPressTime = 0;
bool buttonActive = false;
bool feedbackGiven = false;
bool modeChanged = false;
bool shuffleChanged = false; // 셔플 중복 토글 방지
unsigned long lastEncoderMoveTime = 0;
bool isWaitingToPlay = false;
unsigned long songStartTime = 0;
unsigned long pausedTime = 0;
unsigned long lastBarUpdate = 0;
float scrollX = 10;
unsigned long lastScrollTime = 0;
int titlePixelWidth = 0;
// --- 볼륨 표시 ---
void renderVolumeText(bool erase) {
tft.setTextSize(2);
tft.setCursor(150, 295);
if (erase) {
tft.setTextColor(ILI9341_BLACK, ILI9341_BLACK);
} else {
tft.setTextColor(isVolumeMode ? ILI9341_ORANGE : ILI9341_LIGHTGREY, ILI9341_BLACK);
}
tft.print("VOL:");
int displayVol = map(volume, 0, 30, 0, 100);
if (displayVol < 10) tft.print(" ");
else if (displayVol < 100) tft.print(" ");
tft.print(displayVol);
}
String formatTime(long seconds) {
int m = seconds / 60;
int s = seconds % 60;
return (m < 10 ? "0" : "") + String(m) + ":" + (s < 10 ? "0" : "") + String(s);
}
void setup() {
Serial1.setRX(1);
Serial1.setTX(0);
Serial1.begin(9600);
randomSeed(analogRead(26)); // 셔플용 랜덤 시드
pinMode(ENCODER_S1, INPUT_PULLUP);
pinMode(ENCODER_S2, INPUT_PULLUP);
pinMode(ENCODER_KEY, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENCODER_S1), readEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENCODER_S2), readEncoder, CHANGE);
pinMode(TFT_CS, OUTPUT);
pinMode(SD_CS, OUTPUT);
digitalWrite(TFT_CS, HIGH);
digitalWrite(SD_CS, HIGH);
tft.begin();
tft.setRotation(0);
tft.setTextWrap(false);
tft.fillScreen(ILI9341_BLACK);
tft.setTextColor(0x7BEF); tft.setTextSize(1);
tft.setCursor(92, 125); tft.print("PRESENT");
tft.setTextColor(ILI9341_WHITE); tft.setTextSize(3);
tft.setCursor(45, 145); tft.print("Seojeong");
tft.drawFastHLine(85, 180, 70, 0x07FF);
bool sdReady = false;
for (int i = 0; i < 10; i++) {
if (SD.begin(SD_CS)) {
sdReady = true;
break;
}
delay(150);
}
if (sdReady) {
File listFile = SD.open("list.txt");
if (listFile) {
totalTracks = 0;
while (listFile.available() && totalTracks < 100) {
String line = listFile.readStringUntil('\n');
line.trim();
int firstDash = line.indexOf('-');
int lastDash = line.lastIndexOf('-');
if (firstDash != -1 && lastDash != firstDash) {
playlist[totalTracks].artist = line.substring(0, firstDash);
playlist[totalTracks].title = line.substring(firstDash + 1, lastDash);
String timeStr = line.substring(lastDash + 1);
int colon = timeStr.indexOf(':');
if (colon != -1) {
int m = timeStr.substring(0, colon).toInt();
int s = timeStr.substring(colon + 1).toInt();
playlist[totalTracks].totalSeconds = (m * 60) + s;
}
playlist[totalTracks].artist.trim();
playlist[totalTracks].title.trim();
totalTracks++;
}
}
listFile.close();
}
}
if (totalTracks > 0) {
// 1부터 totalTracks 사이의 숫자를 무작위로 선택
currentTrack = random(1, totalTracks + 1);
}
if (myDFPlayer.begin(Serial1)) {
// [수정] 부팅 직후 볼륨이 튀는 것을 방지하기 위해 초기화 지연 후 설정
delay(500);
myDFPlayer.volume(volume);
delay(100);
}
delay(1200);
updateUI();
}
void loop() {
static unsigned long lastSDCheck = 0;
if (millis() - lastSDCheck > 2000) { // 2초마다 체크 (성능 저하 방지)
lastSDCheck = millis();
if (!SD.exists("/")) { // 루트 경로가 보이지 않으면 카드 제거로 판단
handleSDError();
return; // 에러 상태일 때는 아래 기능을 수행하지 않음
}
}
checkEncoder();
handleButton();
handleSmartSelect();
updateProgressBar();
drawVolumeBlink();
drawVisualizer();
updateTitleScroll();
}
void playNextTrack() {
if (isShuffle) {
currentTrack = random(1, totalTracks + 1);
} else {
currentTrack++;
if (currentTrack > totalTracks) currentTrack = 1;
}
playTrack(currentTrack);
}
void updateTitleScroll() {
if (totalTracks == 0) return;
unsigned long now = millis();
if (now - lastScrollTime > 30) {
lastScrollTime = now;
// --- 제목(Title) 스크롤 로직 (기존) ---
if (titlePixelWidth > 220) {
scrollX -= 2;
if (scrollX < -(titlePixelWidth + 40)) scrollX = 240;
tft.setTextSize(3);
tft.setTextColor(ILI9341_YELLOW, ILI9341_BLACK);
tft.setCursor((int)scrollX, 55);
tft.print(playlist[currentTrack - 1].title);
tft.print(" ");
}
// --- 가수(Artist) 스크롤 로직 (추가) ---
if (artistPixelWidth > 220) {
artistScrollX -= 1.5; // 제목보다 약간 느리게 설정 가능
if (artistScrollX < -(artistPixelWidth + 40)) artistScrollX = 240;
tft.setTextSize(2);
tft.setTextColor(0xBDD7, ILI9341_BLACK);
tft.setCursor((int)artistScrollX, 95); // Y좌표 95 유지
tft.print(playlist[currentTrack - 1].artist);
tft.print(" ");
}
// 검은색 잔상 방지 (화면 끝 처리)
if (scrollX < 0 && scrollX > -20) tft.fillRect(0, 50, 10, 35, ILI9341_BLACK);
if (artistScrollX < 0 && artistScrollX > -20) tft.fillRect(0, 90, 10, 25, ILI9341_BLACK);
}
}
void checkEncoder() {
if (encoderPos != lastReportedPos) {
int diff = (encoderPos - lastReportedPos) / 4;
if (diff != 0) {
onEncoderMove(diff);
lastReportedPos = encoderPos;
if (!isVolumeMode) {
lastEncoderMoveTime = millis();
isWaitingToPlay = true;
}
}
}
}
void onEncoderMove(int dir) {
if (isVolumeMode) {
volume = constrain(volume + dir, 0, 30);
myDFPlayer.volume(volume);
renderVolumeText(false);
} else {
currentTrack += dir;
if (currentTrack > totalTracks) currentTrack = 1;
if (currentTrack < 1) currentTrack = totalTracks;
updateUI();
}
}
void handleButton() {
bool btnState = (digitalRead(ENCODER_KEY) == LOW);
if (btnState) {
if (!buttonActive) {
buttonActive = true;
buttonPressTime = millis();
feedbackGiven = false;
}
// --- [추가] 실시간 하단 가이드 메시지 ---
unsigned long pressDur = millis() - buttonPressTime;
tft.setTextSize(2);
if (pressDur > 3000) { // 3초 경과
tft.setTextColor(ILI9341_CYAN, ILI9341_BLACK);
tft.setCursor(10, 295);
tft.print("[SHUFFLE?] "); // 3초 후 떼면 셔플
}
else if (pressDur > 1000) { // 1초 경과
tft.setTextColor(ILI9341_ORANGE, ILI9341_BLACK);
tft.setCursor(10, 295);
tft.print("[VOL MODE?]"); // 1초 후 떼면 볼륨
}
}
else { // 버튼을 뗐을 때
if (buttonActive) {
unsigned long duration = millis() - buttonPressTime;
// 동작 실행 후 하단 가이드 영역 지우기
tft.fillRect(0, 290, 130, 30, ILI9341_BLACK);
if (duration > 3000) {
isShuffle = !isShuffle;
updateUI();
} else if (duration > 1000) {
isVolumeMode = !isVolumeMode;
if (!isVolumeMode) showSaveMsg();
updateUI();
} else if (duration > 50) {
togglePause();
}
buttonActive = false;
feedbackGiven = false;
}
}
}
void handleSmartSelect() {
if (isWaitingToPlay && !isVolumeMode) {
if (millis() - lastEncoderMoveTime > 1500) {
playTrack(currentTrack);
isWaitingToPlay = false;
}
}
}
void playTrack(int trackNum) {
myDFPlayer.play(trackNum);
isPaused = false;
isWaitingToPlay = false;
songStartTime = millis();
updateUI();
}
void togglePause() {
if (isPaused) {
if (songStartTime == 0) playTrack(currentTrack);
else {
myDFPlayer.start();
songStartTime += (millis() - pausedTime);
isPaused = false;
}
} else {
myDFPlayer.pause(); pausedTime = millis(); isPaused = true;
}
updateUI();
}
void updateUI() {
tft.fillScreen(ILI9341_BLACK);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.setTextSize(2);
tft.setCursor(10, 20);
if (isWaitingToPlay) tft.print(">> SELECT");
else {
tft.print(isPaused ? "PAUSED" : "PLAYING");
if (isShuffle) {
tft.setTextColor(ILI9341_CYAN);
tft.print(" [SHUFFLE]"); // 셔플 활성화 표시
tft.setTextColor(ILI9341_WHITE);
}
}
if (totalTracks > 0) {
String currentTitle = playlist[currentTrack - 1].title;
titlePixelWidth = currentTitle.length() * 18;
scrollX = 10;
tft.setTextSize(3);
tft.setTextColor(ILI9341_YELLOW, ILI9341_BLACK);
tft.setCursor(10, 55);
tft.print(currentTitle);
String currentArtist = playlist[currentTrack - 1].artist;
artistPixelWidth = currentArtist.length() * 12; // TextSize 2이므로 글자당 약 12픽셀
artistScrollX = 10;
if (artistPixelWidth <= 220) {
tft.setTextSize(2);
tft.setTextColor(0xBDD7, ILI9341_BLACK);
tft.setCursor(10, 95);
tft.print(currentArtist);
}
}
tft.drawRect(18, 146, 204, 12, ILI9341_WHITE);
renderVolumeText(false);
}
void updateProgressBar() {
if (totalTracks == 0) return;
if (myDFPlayer.readType() == DFPlayerError) {
isPaused = true;
return;
}
if (millis() - lastBarUpdate > 500) {
lastBarUpdate = millis();
long elapsed = (isPaused || songStartTime == 0) ? (pausedTime - songStartTime) / 1000 : (millis() - songStartTime) / 1000;
if (songStartTime == 0) elapsed = 0;
long total = playlist[currentTrack - 1].totalSeconds;
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.setCursor(20, 135); tft.print(formatTime(elapsed));
tft.setCursor(190, 135); tft.print(formatTime(total));
if (total > 0) {
int barWidth = map(constrain(elapsed, 0, total), 0, total, 0, 200);
tft.fillRect(20, 148, barWidth, 8, ILI9341_CYAN);
tft.fillRect(20 + barWidth, 148, 200 - barWidth, 8, 0x2104);
// 곡이 끝났을 때 자동 다음 곡 재생 로직
if (!isPaused && elapsed >= total) {
delay(500);
playNextTrack();
}
}
}
}
void drawVisualizer() {
if (isPaused) {
tft.fillRect(20, 210, 200, 45, ILI9341_BLACK);
return;
}
static unsigned long lastVis = 0;
if (millis() - lastVis > 120) {
lastVis = millis();
for (int i = 0; i < 8; i++) {
int h = random(5, 40);
int x = 25 + (i * 25);
tft.fillRect(x, 210, 15, 40, ILI9341_BLACK);
tft.fillRect(x, 250 - h, 15, h, 0xF81F);
}
}
}
void drawVolumeBlink() {
static unsigned long lastBlink = 0;
static bool show = true;
if (!isVolumeMode) return;
if (millis() - lastBlink > 400) {
lastBlink = millis();
show = !show;
renderVolumeText(!show);
}
}
void showSaveMsg() {
tft.fillRect(0, 285, 240, 35, ILI9341_DARKGREEN);
tft.setCursor(35, 295);
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(2);
tft.print("SETTINGS SAVED");
delay(800);
}
void handleSDError() {
tft.fillScreen(ILI9341_RED);
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(2);
tft.setCursor(30, 140);
tft.print("SD CARD REMOVED!");
tft.setCursor(20, 170);
tft.print("Please Re-insert...");
myDFPlayer.stop(); // 재생 중단 명령
while (!SD.begin(SD_CS)) {
delay(500); // 재삽입 대기
}
updateUI(); // 복구 시 UI 갱신
}