diff --git a/FM_Brocaster.ino b/FM_Brocaster.ino index 8f623c1..b5b54b4 100644 --- a/FM_Brocaster.ino +++ b/FM_Brocaster.ino @@ -1,407 +1,514 @@ -/* -ESP32 Frequency Modulation Broadcaster - -Some code is from my old game project but now is almost dead. - -Wei - 2026 - -You can do anything you want to this source code. BUT you MUST cite the original soruce of the code... -*/ #include +#include #include #include #include "KT0803.h" +#include +#include "MP3DecoderHelix.h" + +using namespace libhelix; + +#define VERSION "1.2" +#define OLED_CS 5 +#define OLED_DC 27 +#define OLED_RST 14 +#define JOY_X 34 +#define JOY_Y 35 +#define JOY_SW 32 +#define SD_MOSI 23 +#define SD_MISO 19 +#define SD_SCK 18 +#define SD_CS 33 +#define I2S_BCLK 26 +#define I2S_LRCK 25 +#define I2S_DOUT 17 + +constexpr uint32_t SAMPLE_RATE = 44100; +constexpr float TONE_FREQ = 440.0f; +constexpr int MAX_MUSIC_FILES = 20; +constexpr int MAX_FILENAME_LEN = 48; KT0803L fm(&Wire); +I2SClass I2S; +U8G2_SH1106_128X64_NONAME_F_4W_HW_SPI u8g2(U8G2_R0, OLED_CS, OLED_DC, OLED_RST); -// ======================= Display ======================= -#define SCREEN_W 128 -#define SCREEN_H 64 - -#define OLED_MOSI 23 // SDA -#define OLED_CLK 18 // SCK -#define OLED_DC 27 -#define OLED_CS 5 -#define OLED_RST 14 // RES - -U8G2_SH1106_128X64_NONAME_F_4W_HW_SPI u8g2( - U8G2_R0, - OLED_CS, - OLED_DC, - OLED_RST -); - -// ======================= Joystick Pins ======================= -#define JOY_X 4 -#define JOY_Y 13 -#define JOY_SW 2 - -#define FM_SDA 21 -#define FM_SCL 22 - -typedef struct CursorPos { - int x; - int y; -} CursorPos; - -void setup() { - // debug - Serial.begin(115200); - - // Initialize display - u8g2.begin(); - - // fm module - Wire.begin(); - Wire.setClock(100000); - - // scan i2c devices - byte error, address; - int nDevices; - - for(address = 1; address < 127; address++ ) { - Wire.beginTransmission(address); - error = Wire.endTransmission(); - if (error == 0) { - Serial.print("I2C device found at address 0x"); - if (address<16) Serial.print("0"); - Serial.println(address,HEX); - nDevices++; - } - } - if (nDevices == 0) Serial.println("No I2C devices found\n"); - - // Set font and en-able unicode support - u8g2.enableUTF8Print(); - u8g2.setFont(u8g2_font_6x12_tf); - - u8g2.clearBuffer(); - - // Joystick - pinMode(JOY_SW, INPUT_PULLUP); - analogReadResolution(12); - - // Set center position of the joystick - Serial.println("Correcting joystick... (Don't touch joystick when processing...)"); - - int rawX = analogRead(JOY_X); - int rawY = analogRead(JOY_Y); - int cx = 2048; - int cy = 2048; - - Serial.println("Initialization Finished"); -} - -void printScreen(int x, int y, const char* text, bool clear_screen, bool send_buffer) { - if (clear_screen) { - u8g2.clearBuffer(); - } - - u8g2.setCursor(x, y); - u8g2.print(text); - - if (send_buffer) { - u8g2.sendBuffer(); - } -} - -struct JoyStickStatus; - -JoyStickStatus getJoyStickerStatus(int pin_x, int pin_y, int pin_sw); - -struct JoyStickStatus { - bool up; - bool down; - bool left; - bool right; - bool pressed; -}; - -JoyStickStatus getJoyStickerStatus(int pin_x, int pin_y, int pin_sw) { - JoyStickStatus status = {0}; - - int x = analogRead(pin_x); - int y = analogRead(pin_y); - bool pressed = (digitalRead(pin_sw) == LOW); - - if (x >= 1800 && y >= 3800) { - Serial.println("down"); - status.down = true; - - } - - if (x >= 1800 && y <= 500) { - Serial.println("up"); - status.up = true; - } - - if (x <= 500 && y >= 1800) { - Serial.println("left"); - status.left = true; - } - - if (x >= 3800 && y >= 1800) { - Serial.println("right"); - status.right = true; - } - - status.pressed = pressed; - - char buffer[100]; - snprintf(buffer, sizeof(buffer), "X:%d , Y:%d, SW:%s", x, y, pressed ? "PRESSED" : "RELEASED"); - Serial.println(buffer); - - return status; -} - -void getJoyStickerDebugInfo(int start_x, int start_y, int pin_x, int pin_y, int pin_sw, bool print_in_serial) { - int x = analogRead(pin_x); - int y = analogRead(pin_y); - bool pressed = (digitalRead(pin_sw) == LOW); - - u8g2.setCursor(start_x, start_y); - u8g2.print("X: "); - u8g2.print(x); - - u8g2.setCursor(start_x, start_y + 12); - u8g2.print("Y: "); - u8g2.print(y); - - u8g2.setCursor(start_x, start_y + 24); - u8g2.print("SW: "); - u8g2.print(pressed ? "PRESSED" : "RELEASED"); - - JoyStickStatus status = getJoyStickerStatus(JOY_X, JOY_Y, JOY_SW); - - u8g2.setCursor(start_x, start_y + 36); - if (status.up) { - u8g2.print("Direction: UP"); - } - else if (status.down) { - u8g2.print("Direction: DOWN"); - } - else if (status.left) { - u8g2.print("Direction: LEFT"); - } - else if (status.right) { - u8g2.print("Direction: RIGHT"); - } - - if (print_in_serial) { - char buffer[100]; - snprintf(buffer, sizeof(buffer), "X:%d , Y:%d, SW:%s", x, y, pressed ? "PRESSED" : "RELEASED"); - Serial.println(buffer); - } -} +struct CursorPos { int x, y; }; +struct JoyStickStatus { bool up, down, left, right, pressed; }; +bool swapJoyStickVertical = false; +bool swapJoyStickHorizontal = false; +int dacVolume = 12; int selectOptionID = 1; -int currentOptionPage = 0; // 0: Home, 1: JoyStick Debug, 2: FM Broadcast -const int optionIDList[] = {1,2}; +int currentOptionPage = 0; int buttonPressCount = 0; bool wasPressed = false; unsigned long buttonPressTime = 0; -float roundToOneDecimal(float num) { - return (int)(num * 10 + 0.5) / 10.0; -} +const char* menuNames[] = { + "Debug Joystick", "FM Brocaster", "Debug PCM5102", "Volume", "Play Music" +}; +constexpr int OPTION_COUNT = sizeof(menuNames) / sizeof(menuNames[0]); -void hanleQuitMenuEvent() { - JoyStickStatus joyStatus = getJoyStickerStatus(JOY_X, JOY_Y, JOY_SW); +char musicFiles[MAX_MUSIC_FILES][MAX_FILENAME_LEN]; +int musicFileCount = 0; +int selectedMusic = 0; - if (joyStatus.pressed && !wasPressed) { - // Start time record - buttonPressTime = millis(); - wasPressed = true; +File playingFile; +bool musicPlaying = false; +bool musicButtonWasPressed = false; +uint32_t currentAudioRate = SAMPLE_RATE; + +void mp3DataCallback(MP3FrameInfo &info, int16_t *pcm, size_t len, void*) { + if (!pcm || !len || info.nChans < 1) return; + + if ((uint32_t)info.samprate != currentAudioRate) { + if (I2S.configureTX(info.samprate, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO)) + currentAudioRate = info.samprate; } - if (!joyStatus.pressed && wasPressed) { - // Quit menu if pressed time more than over 3 sec - unsigned long duration = millis() - buttonPressTime; + constexpr size_t CHUNK_SAMPLES = 256; + int16_t out[CHUNK_SAMPLES * 2]; - if (duration >= 1500) { - currentOptionPage=0; + if (info.nChans == 1) { + for (size_t pos = 0; pos < len;) { + size_t n = min((size_t)CHUNK_SAMPLES, len - pos); + for (size_t i = 0; i < n; i++) { + int32_t v = (int32_t)pcm[pos + i] * dacVolume / 255; + out[i * 2] = out[i * 2 + 1] = (int16_t)v; + } + I2S.write( + reinterpret_cast(out), + n * 2 * sizeof(int16_t) + ); + pos += n; } + } else { + for (size_t pos = 0; pos < len;) { + size_t n = min((size_t)(CHUNK_SAMPLES * 2), len - pos); + for (size_t i = 0; i < n; i++) + out[i] = (int16_t)((int32_t)pcm[pos + i] * dacVolume / 255); + I2S.write( + reinterpret_cast(out), + n * sizeof(int16_t) + ); + pos += n; + } + } +} +MP3DecoderHelix mp3(mp3DataCallback); + +void printScreen(int x, int y, const char* text, bool clearScreen = true, bool send = true) { + if (clearScreen) u8g2.clearBuffer(); + u8g2.setCursor(x, y); + u8g2.print(text); + if (send) u8g2.sendBuffer(); +} + +int readJoystickAxis(int pin) { + int sum = 0; + for (int i = 0; i < 4; i++) sum += analogRead(pin); + return sum / 4; +} + +JoyStickStatus getJoyStickerStatus() { + constexpr int CENTER_LOW = 1500, CENTER_HIGH = 2600; + int x = readJoystickAxis(JOY_X), y = readJoystickAxis(JOY_Y); + JoyStickStatus s = { + x >= CENTER_LOW && x <= CENTER_HIGH && y < 500, + x >= CENTER_LOW && x <= CENTER_HIGH && y > 3800, + y >= CENTER_LOW && y <= CENTER_HIGH && x < 500, + y >= CENTER_LOW && y <= CENTER_HIGH && x > 3800, + digitalRead(JOY_SW) == LOW + }; + if (swapJoyStickVertical) std::swap(s.up, s.down); + if (swapJoyStickHorizontal) std::swap(s.left, s.right); + return s; +} + +void getJoyStickerDebugInfo(bool serialOut) { + int x = readJoystickAxis(JOY_X), y = readJoystickAxis(JOY_Y); + bool pressed = digitalRead(JOY_SW) == LOW; + JoyStickStatus s = getJoyStickerStatus(); + + u8g2.setCursor(0, 26); u8g2.print("X: "); u8g2.print(x); + u8g2.setCursor(0, 38); u8g2.print("Y: "); u8g2.print(y); + u8g2.setCursor(0, 50); u8g2.print("SW: "); u8g2.print(pressed ? "PRESSED" : "RELEASED"); + u8g2.setCursor(0, 62); + if (s.up) u8g2.print("Direction: UP"); + else if (s.down) u8g2.print("Direction: DOWN"); + else if (s.left) u8g2.print("Direction: LEFT"); + else if (s.right) u8g2.print("Direction: RIGHT"); + + if (serialOut) { + Serial.print("X:"); Serial.print(x); + Serial.print(" , Y:"); Serial.print(y); + Serial.print(", SW:"); Serial.println(pressed ? "PRESSED" : "RELEASED"); + } +} + +float roundToOneDecimal(float n) { return (int)(n * 10 + 0.5f) / 10.0f; } + +void hanleQuitMenuEvent() { + bool pressed = getJoyStickerStatus().pressed; + if (pressed && !wasPressed) { + buttonPressTime = millis(); + wasPressed = true; + } else if (!pressed && wasPressed) { + if (millis() - buttonPressTime >= 1000) currentOptionPage = 0; wasPressed = false; } } -void setFM(float freq, bool mute) { +void setFM(float freq, bool mute = false) { fm.setUSA(); fm.setFrequency(freq); fm.setMute(mute); } -void fmBroadcast(void) { - float fm_freq = 98.1; - float old_freq = 98.1; +void drawFM(float freq, bool frame) { + printScreen(0, 14, frame ? "Brocasting_" : "Brocasting ", true, false); + u8g2.setCursor(0, 26); u8g2.print("Frequency: "); u8g2.print(freq); + u8g2.setCursor(0, 38); u8g2.print("Now you can leave"); + u8g2.setCursor(0, 50); u8g2.print("this page."); + u8g2.sendBuffer(); +} - if (!fm.begin(fm_freq, false)) { - Serial.println("FM Module KT0803 not found. Exiting..."); +void fmBroadcast() { + float freq = 98.1f, oldFreq = freq; + if (!fm.begin(freq, false)) { printScreen(0, 14, "FM Module KT0803 not", true, false); - u8g2.setCursor(0, 26); - u8g2.print(" found. Exiting..."); + u8g2.setCursor(0, 26); u8g2.print(" found. Exiting..."); u8g2.sendBuffer(); delay(3500); - currentOptionPage=0; + currentOptionPage = 0; return; } - - setFM(fm_freq, false); - + setFM(freq); + bool frame = false; while (currentOptionPage == 2) { - int adc = analogRead(JOY_X); - int offset = adc - 2048; - - if (abs(offset) > 200) - { - float speed = offset / 2048.0f; - - fm_freq += speed * 0.1f; - fm_freq = roundToOneDecimal(fm_freq); - - // Max and min value check - if (fm_freq < 88.0) { - fm_freq = 88.0; - } - else if (fm_freq > 108.0) { - fm_freq = 108.0; - } + int offset = analogRead(JOY_X) - 2048; + if (abs(offset) > 200) { + freq = roundToOneDecimal(freq + (offset / 2048.0f) * 0.1f); + freq = constrain(freq, 88.0f, 108.0f); } - - if (old_freq != fm_freq) { - // Update frequency - setFM(fm_freq, false); - old_freq = fm_freq; - Serial.print("Frequency: "); - Serial.println(fm_freq); + if (oldFreq != freq) { + setFM(freq); + oldFreq = freq; + Serial.print("Frequency: "); Serial.println(freq); } - - printScreen(0, 14, "Brocasting_", true, false); - u8g2.setCursor(0, 26); - u8g2.print("Frequency: "); - u8g2.print(fm.getFrequency()); - u8g2.setCursor(0, 38); - u8g2.print("Now you can leave"); - u8g2.setCursor(0, 50); - u8g2.print("this page."); - u8g2.sendBuffer(); - delay(300); - printScreen(0, 14, "Brocasting ", true, false); - u8g2.setCursor(0, 26); - u8g2.print("Frequency: "); - u8g2.print(fm.getFrequency()); - u8g2.setCursor(0, 38); - u8g2.print("Now you can leave"); - u8g2.setCursor(0, 50); - u8g2.print("this page."); - u8g2.sendBuffer(); + drawFM(fm.getFrequency(), frame = !frame); delay(300); hanleQuitMenuEvent(); } } -void joyStickDebugMenu(void) { +void joyStickDebugMenu() { + int dots = 1; + char title[24]; while (currentOptionPage == 1) { - printScreen(0, 14, "Debugging joystick.", true, false); - getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true); - u8g2.sendBuffer(); - delay(300); - printScreen(0, 14, "Debugging joystick..", true, false); - getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true); - u8g2.sendBuffer(); - delay(300); - printScreen(0, 14, "Debugging joystick...", true, false); - getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true); - u8g2.sendBuffer(); - delay(300); - printScreen(0, 14, "Debugging joystick....", true, false); - getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true); + strcpy(title, "Debugging joystick"); + for (int i = 0; i < dots; i++) strcat(title, "."); + printScreen(0, 14, title, true, false); + getJoyStickerDebugInfo(true); u8g2.sendBuffer(); + dots = dots % 4 + 1; delay(300); hanleQuitMenuEvent(); } } -CursorPos printOptionMenu(int lastX, int lastY) { - CursorPos pos = {lastX, lastY}; - - pos.y+=12; - u8g2.setCursor(pos.x, pos.y); - u8g2.print("1: Debug Joystick"); - if (selectOptionID == 1) { - u8g2.print(" [X]"); - } else { - u8g2.print(" [ ]"); - } - - pos.y+=12; - u8g2.setCursor(pos.x, pos.y); - u8g2.print("2: FM Broadcast"); - if (selectOptionID == 2) { - u8g2.print(" [X]"); - } else { - u8g2.print(" [ ]"); +CursorPos printOptionMenu(int x, int y, int availableHeight, int textSize) { + CursorPos pos = {x, y}; + int maxVisible = availableHeight / textSize; + int start = selectOptionID - maxVisible / 2; + start = constrain(start, 0, max(0, OPTION_COUNT - maxVisible)); + int end = min(start + maxVisible, OPTION_COUNT); + for (int i = start; i < end; i++) { + pos.y += 12; + u8g2.setCursor(pos.x, pos.y); + u8g2.print(i + 1); u8g2.print(": "); u8g2.print(menuNames[i]); + u8g2.print(selectOptionID == i + 1 ? " [X]" : " [ ]"); } return pos; } void handleOptionSelectEvent() { - JoyStickStatus joyStatus = getJoyStickerStatus(JOY_X, JOY_Y, JOY_SW); - size_t lengthOfOptions = sizeof(optionIDList) / sizeof(optionIDList[0]); - - if (joyStatus.down) { - if (selectOptionID+1 <= lengthOfOptions) { - selectOptionID+=1; - } - } - else if (joyStatus.up) { - if (selectOptionID-1 >= 1) { - selectOptionID-=1; - } - } - - if (joyStatus.pressed) { - buttonPressCount+=1; - } - - // Do option page replace if button is pressed twice. - if (buttonPressCount>=2) { - buttonPressCount=0; - currentOptionPage=selectOptionID; - - // Reset - selectOptionID=1; + JoyStickStatus s = getJoyStickerStatus(); + if (s.down && selectOptionID < OPTION_COUNT) selectOptionID++; + else if (s.up && selectOptionID > 1) selectOptionID--; + if (s.pressed) buttonPressCount++; + if (buttonPressCount >= 2) { + buttonPressCount = 0; + currentOptionPage = selectOptionID; + selectOptionID = 1; } } -void homeMenu(void) { - while (currentOptionPage==0) { - printScreen(0, 14, "Running_", true, false); - CursorPos currentPos = printOptionMenu(0, 14); - printScreen(currentPos.x, currentPos.y+24, "FM Brocaster v1.0", false, false); - u8g2.sendBuffer(); - delay(300); +void printLogo(int x, int y) { + u8g2.drawBox(x, y, 128 - x, 1); + u8g2.setCursor(x + 2, y + 10); + u8g2.print("FM Brocaster v" VERSION); +} - // Handle option select event while the joystick is moving (?) - handleOptionSelectEvent(); - - printScreen(0, 14, "Running ", true, false); - currentPos = printOptionMenu(0, 14); - printScreen(currentPos.x, currentPos.y+24, "FM Brocaster v1.0", false, false); - u8g2.sendBuffer(); - delay(300); +void homeMenu() { + bool frame = false; + unsigned long displayAt = millis(), inputAt = millis(), animateAt = millis(); + while (currentOptionPage == 0) { + unsigned long now = millis(); + if (now - inputAt >= 500) { inputAt = now; handleOptionSelectEvent(); } + if (now - animateAt >= 500) { animateAt = now; frame = !frame; } + if (now - displayAt >= 33) { + displayAt = now; + printScreen(0, 14, frame ? "Running_" : "Running ", true, false); + CursorPos p = printOptionMenu(0, 14, 36, 12); + printLogo(p.x, p.y + 2); + u8g2.sendBuffer(); + } + delay(1); } } -void loop(void) { - if (currentOptionPage == 0) { - homeMenu(); +void test_pcm5102(float sineFreq, float squareFreq, float triangleFreq) { + static float phase[3] = {0, 0, 0}; + constexpr size_t FRAMES = 256; + int16_t samples[FRAMES * 2]; + float step[3] = { + TWO_PI * sineFreq / SAMPLE_RATE, + TWO_PI * squareFreq / SAMPLE_RATE, + TWO_PI * triangleFreq / SAMPLE_RATE + }; + + for (size_t i = 0; i < FRAMES; i++) { + float mixed = 0; + if (sineFreq > 0) mixed += sinf(phase[0]) * 2500.0f; + if (squareFreq > 0) mixed += (phase[1] < PI ? 1.0f : -1.0f) * 1000.0f; + if (triangleFreq > 0) + mixed += (phase[2] < PI ? 2.0f * phase[2] / PI - 1.0f : 3.0f - 2.0f * phase[2] / PI) * 1000.0f; + + int16_t sample = (int16_t)constrain(mixed, -32768.0f, 32767.0f); + samples[i * 2] = samples[i * 2 + 1] = sample; + for (int p = 0; p < 3; p++) { + phase[p] += step[p]; + if (phase[p] >= TWO_PI) phase[p] -= TWO_PI; + } } - else if (currentOptionPage == 1) { - joyStickDebugMenu(); + I2S.write((uint8_t*)samples, sizeof(samples)); +} + +void pcm5102DebugMenu() { + unsigned long displayAt = 0; + int frame = 0; + float sine = TONE_FREQ, square = 0, triangle = 0; + while (currentOptionPage == 3) { + test_pcm5102(sine, square, triangle); + int offset = readJoystickAxis(JOY_X) - 2048; + if (abs(offset) > 200) sine = roundToOneDecimal(sine + (offset / 2048.0f) * 0.1f); + + offset = readJoystickAxis(JOY_Y) - 2048; + float speed = abs(offset) / 2048.0f * 0.1f; + if (offset > 0) triangle = roundToOneDecimal(triangle + speed); + else if (offset < 0) square = roundToOneDecimal(square + speed); + + if (millis() - displayAt >= 300) { + displayAt = millis(); + char title[24] = "Debugging PCM5102"; + for (int i = 0; i <= frame; i++) strcat(title, "."); + frame = (frame + 1) % 4; + printScreen(0, 14, title, true, false); + u8g2.setCursor(0, 28); u8g2.print("Sine Freq: "); u8g2.print(sine); + u8g2.setCursor(0, 40); u8g2.print("Square Freq: "); u8g2.print(square); + u8g2.setCursor(0, 52); u8g2.print("Triangle Freq: "); u8g2.print(triangle); + u8g2.sendBuffer(); + hanleQuitMenuEvent(); + } } - else if (currentOptionPage == 2) { - fmBroadcast(); +} + +void volumeMenu() { + unsigned long updateAt = 0; + while (currentOptionPage == 4) { + if (millis() - updateAt < 30) continue; + updateAt = millis(); + JoyStickStatus s = getJoyStickerStatus(); + dacVolume = constrain(dacVolume + (s.left ? 1 : s.right ? -1 : 0), 0, 255); + int barWidth = dacVolume * 84 / 255; + u8g2.clearBuffer(); + u8g2.setCursor(0, 14); u8g2.print("Volume"); + u8g2.drawFrame(20, 30, 88, 12); + u8g2.drawBox(22, 32, barWidth, 8); + u8g2.sendBuffer(); + hanleQuitMenuEvent(); + delay(1); } -} \ No newline at end of file +} + +void scanMusicFiles() { + musicFileCount = 0; + File dir = SD.open("/music"); + if (!dir || !dir.isDirectory()) return; + while (musicFileCount < MAX_MUSIC_FILES) { + File file = dir.openNextFile(); + if (!file) break; + if (!file.isDirectory()) { + const char* name = file.name(); + const char* ext = strrchr(name, '.'); + if (ext && !strcasecmp(ext, ".mp3")) { + strncpy(musicFiles[musicFileCount], name, MAX_FILENAME_LEN - 1); + musicFiles[musicFileCount++][MAX_FILENAME_LEN - 1] = '\0'; + } + } + file.close(); + } + dir.close(); +} + +void handleMusicSelect() { + int y = readJoystickAxis(JOY_Y); + if (musicFileCount == 0) return; + if (y < 500) { selectedMusic = (selectedMusic + musicFileCount - 1) % musicFileCount; delay(200); } + else if (y > 3800) { selectedMusic = (selectedMusic + 1) % musicFileCount; delay(200); } +} + +void drawMusicMenu() { + u8g2.clearBuffer(); + u8g2.setCursor(0, 10); u8g2.print("Music"); + if (!musicFileCount) { + u8g2.setCursor(0, 30); u8g2.print("No MP3 files"); + } else { + for (int row = -1; row <= 1; row++) { + int index = selectedMusic + row; + if (index < 0 || index >= musicFileCount) continue; + int y = 32 + row * 12; + if (index == selectedMusic) { u8g2.setCursor(0, y); u8g2.print('>'); } + const char* name = musicFiles[index]; + if (!strncmp(name, "/music/", 7)) name += 7; + char shown[18]; + strncpy(shown, name, 17); shown[17] = '\0'; + u8g2.setCursor(10, y); u8g2.print(shown); + } + } + u8g2.sendBuffer(); +} + +void stopMusic() { + if (playingFile) playingFile.close(); + if (musicPlaying) mp3.end(); + musicPlaying = false; +} + +bool startMusic(int index) { + stopMusic(); + + char path[MAX_FILENAME_LEN + 8]; + const char* name = musicFiles[index]; + if (name[0] == '/') snprintf(path, sizeof(path), "%s", name); + else if (!strncmp(name, "music/", 6)) snprintf(path, sizeof(path), "/%s", name); + else snprintf(path, sizeof(path), "/music/%s", name); + + playingFile = SD.open(path, FILE_READ); + if (!playingFile) { + Serial.print("Unable to open: "); + Serial.println(path); + return false; + } + + Serial.print("Playing: "); + Serial.println(path); + mp3.begin(); + musicPlaying = true; + return true; +} + +void feedMusicDecoder() { + if (!musicPlaying || !playingFile) return; + + uint8_t input[1024]; + int n = playingFile.read(input, sizeof(input)); + if (n > 0) { + mp3.write(input, n); + } else { + stopMusic(); + Serial.println("Playback finished"); + } +} + +void musicMenu() { + scanMusicFiles(); + musicButtonWasPressed = digitalRead(JOY_SW) == LOW; + + while (currentOptionPage == 5) { + if (!musicPlaying) handleMusicSelect(); + drawMusicMenu(); + + bool pressed = digitalRead(JOY_SW) == LOW; + if (pressed && !musicButtonWasPressed && musicFileCount > 0) + startMusic(selectedMusic); + musicButtonWasPressed = pressed; + + feedMusicDecoder(); + hanleQuitMenuEvent(); + delay(1); + } + + stopMusic(); +} + +void setup() { + Serial.begin(115200); + u8g2.begin(); + u8g2.setContrast(255); + u8g2.enableUTF8Print(); + u8g2.setFont(u8g2_font_6x12_tf); + Wire.begin(); + Wire.setClock(100000); + + if (psramFound()) { + Serial.println("PSRAM found!"); + Serial.printf("PSRAM size: %u bytes\n", ESP.getPsramSize()); + Serial.printf("Free PSRAM: %u bytes\n", ESP.getFreePsram()); + } else Serial.println("No PSRAM found."); + + int nDevices = 0; + for (byte address = 1; address < 127; address++) { + Wire.beginTransmission(address); + if (Wire.endTransmission() == 0) { + Serial.printf("I2C device found at address 0x%02X\n", address); + nDevices++; + } + } + if (!nDevices) Serial.println("No I2C devices found"); + + pinMode(JOY_SW, INPUT_PULLUP); + analogReadResolution(12); + + I2S.setPins(I2S_BCLK, I2S_LRCK, I2S_DOUT, -1, -1); + Serial.println(I2S.begin(I2S_MODE_STD, SAMPLE_RATE, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO) + ? "Found PCM5102" : "Unable to init PCM5102!"); + + SPI.begin(SD_SCK, SD_MISO, SD_MOSI); + if (!SD.begin(SD_CS, SPI)) { + Serial.println("Unable to init SD card module."); + return; + } + + if (SD.cardType() == CARD_NONE) Serial.println("No SD card detected."); + else { + Serial.printf("SD Card Size: %llu MB\n", SD.cardSize() / (1024ULL * 1024ULL)); + scanMusicFiles(); + for (int i = 0; i < musicFileCount; i++) Serial.println(musicFiles[i]); + } + Serial.println("Initialization Finished"); +} + +void loop() { + switch (currentOptionPage) { + case 0: homeMenu(); break; + case 1: joyStickDebugMenu(); break; + case 2: fmBroadcast(); break; + case 3: pcm5102DebugMenu(); break; + case 4: volumeMenu(); break; + case 5: musicMenu(); break; + default: currentOptionPage = 0; + } +} diff --git a/FM_Brocaster.new b/FM_Brocaster.new new file mode 100644 index 0000000..e220207 --- /dev/null +++ b/FM_Brocaster.new @@ -0,0 +1,487 @@ +/* +ESP32 Frequency Modulation Broadcaster + +Some code is from my old game project but now is almost dead. + +Wei - 2026 + +You can do anything you want to this source code. BUT you MUST cite the original source of the code. +*/ +#include +#include +#include +#include "KT0803.h" +#include + +KT0803L fm(&Wire); + +// ======================= Display ======================= +#define SCREEN_W 128 +#define SCREEN_H 64 + +#define OLED_MOSI 23 // SDA +#define OLED_CLK 18 // SCK +#define OLED_DC 27 +#define OLED_CS 5 +#define OLED_RST 14 // RES + +U8G2_SH1106_128X64_NONAME_F_4W_HW_SPI u8g2( + U8G2_R0, + OLED_CS, + OLED_DC, + OLED_RST +); + +// ======================= Joystick Pins ======================= +#define JOY_X 34 +#define JOY_Y 35 +#define JOY_SW 32 + +#define FM_SDA 21 +#define FM_SCL 22 + +// ======================= SD (microSD) Card ======================= +#define SD_MOSI 23 // sd reader share spi pin with the display +#define SD_MISO 19 +#define SD_SCK 18 +#define SD_CS 33 + +// ======================= PCM5102A DAC ======================= +#define I2S_BCLK 26 +#define I2S_LRCK 25 +#define I2S_DOUT 17 + +#define SAMPLE_RATE 44100 +#define FREQUENCY 440.0f +#define AMPLITUDE 12000 + +I2SClass I2S; + +typedef struct CursorPos { + int x; + int y; +} CursorPos; + +void printScreen(int x, int y, const char* text, bool clear_screen, bool send_buffer) { + if (clear_screen) { + u8g2.clearBuffer(); + } + + u8g2.setCursor(x, y); + u8g2.print(text); + + if (send_buffer) { + u8g2.sendBuffer(); + } +} + +struct JoyStickStatus; + +// Change its value if your joystick's position is not same as you expected. +bool swapJoyStickVertical = false; +bool swapJoyStickHorizontal = false; + +JoyStickStatus getJoyStickerStatus(int pin_x, int pin_y, int pin_sw); + +struct JoyStickStatus { + bool up; + bool down; + bool left; + bool right; + bool pressed; +}; + +JoyStickStatus getJoyStickerStatus(int pin_x, int pin_y, int pin_sw) { + JoyStickStatus status = {0}; + + int x = analogRead(pin_x); + int y = analogRead(pin_y); + bool pressed = (digitalRead(pin_sw) == LOW); + + bool isUp = x >= 1800 && y <= 500; + bool isDown = x >= 1800 && y >= 3800; + bool isLeft = x <= 500 && y >= 1800; + bool isRight = x >= 3800 && y >= 1800; + + if (swapJoyStickVertical) { + // Wow, c++ have this cool thing + std::swap(isUp, isDown); + } + + if (swapJoyStickHorizontal) { + std::swap(isLeft, isRight); + } + + if (isDown) { + Serial.println("down"); + status.down = true; + } + + if (isUp) { + Serial.println("up"); + status.up = true; + } + + if (isLeft) { + Serial.println("left"); + status.left = true; + } + + if (isRight) { + Serial.println("right"); + status.right = true; + } + + status.pressed = pressed; + + char buffer[100]; + snprintf(buffer, sizeof(buffer), "X:%d , Y:%d, SW:%s", x, y, pressed ? "PRESSED" : "RELEASED"); + Serial.println(buffer); + + return status; +} + +void getJoyStickerDebugInfo(int start_x, int start_y, int pin_x, int pin_y, int pin_sw, bool print_in_serial) { + int x = analogRead(pin_x); + int y = analogRead(pin_y); + bool pressed = (digitalRead(pin_sw) == LOW); + + u8g2.setCursor(start_x, start_y); + u8g2.print("X: "); + u8g2.print(x); + + u8g2.setCursor(start_x, start_y + 12); + u8g2.print("Y: "); + u8g2.print(y); + + u8g2.setCursor(start_x, start_y + 24); + u8g2.print("SW: "); + u8g2.print(pressed ? "PRESSED" : "RELEASED"); + + JoyStickStatus status = getJoyStickerStatus(JOY_X, JOY_Y, JOY_SW); + + u8g2.setCursor(start_x, start_y + 36); + if (status.up) { + u8g2.print("Direction: UP"); + } + else if (status.down) { + u8g2.print("Direction: DOWN"); + } + else if (status.left) { + u8g2.print("Direction: LEFT"); + } + else if (status.right) { + u8g2.print("Direction: RIGHT"); + } + + if (print_in_serial) { + char buffer[100]; + snprintf(buffer, sizeof(buffer), "X:%d , Y:%d, SW:%s", x, y, pressed ? "PRESSED" : "RELEASED"); + Serial.println(buffer); + } +} + +int selectOptionID = 1; +int currentOptionPage = 0; // 0: Home, 1: JoyStick Debug, 2: FM Broadcast +const int optionIDList[] = {1,2}; +int buttonPressCount = 0; +bool wasPressed = false; +unsigned long buttonPressTime = 0; + +float roundToOneDecimal(float num) { + return (int)(num * 10 + 0.5) / 10.0; +} + +void hanleQuitMenuEvent() { + JoyStickStatus joyStatus = getJoyStickerStatus(JOY_X, JOY_Y, JOY_SW); + + if (joyStatus.pressed && !wasPressed) { + // Start time record + buttonPressTime = millis(); + wasPressed = true; + } + + if (!joyStatus.pressed && wasPressed) { + // Quit menu if pressed time more than over 1.5 sec + unsigned long duration = millis() - buttonPressTime; + + if (duration >= 1500) { + currentOptionPage=0; + } + + wasPressed = false; + } +} + +void setFM(float freq, bool mute) { + fm.setUSA(); + fm.setFrequency(freq); + fm.setMute(mute); +} + +void fmBroadcast(void) { + float fm_freq = 98.1; + float old_freq = 98.1; + + if (!fm.begin(fm_freq, false)) { + Serial.println("FM Module KT0803 not found. Exiting..."); + printScreen(0, 14, "FM Module KT0803 not", true, false); + u8g2.setCursor(0, 26); + u8g2.print(" found. Exiting..."); + u8g2.sendBuffer(); + delay(3500); + currentOptionPage=0; + return; + } + + setFM(fm_freq, false); + + while (currentOptionPage == 2) { + int adc = analogRead(JOY_X); + int offset = adc - 2048; + + if (abs(offset) > 200) + { + float speed = offset / 2048.0f; + + fm_freq += speed * 0.1f; + fm_freq = roundToOneDecimal(fm_freq); + + // Max and min value check + if (fm_freq < 88.0) { + fm_freq = 88.0; + } + else if (fm_freq > 108.0) { + fm_freq = 108.0; + } + } + + if (old_freq != fm_freq) { + // Update frequency + setFM(fm_freq, false); + old_freq = fm_freq; + Serial.print("Frequency: "); + Serial.println(fm_freq); + } + + printScreen(0, 14, "Brocasting_", true, false); + u8g2.setCursor(0, 26); + u8g2.print("Frequency: "); + u8g2.print(fm.getFrequency()); + u8g2.setCursor(0, 38); + u8g2.print("Now you can leave"); + u8g2.setCursor(0, 50); + u8g2.print("this page."); + u8g2.sendBuffer(); + delay(300); + printScreen(0, 14, "Brocasting ", true, false); + u8g2.setCursor(0, 26); + u8g2.print("Frequency: "); + u8g2.print(fm.getFrequency()); + u8g2.setCursor(0, 38); + u8g2.print("Now you can leave"); + u8g2.setCursor(0, 50); + u8g2.print("this page."); + u8g2.sendBuffer(); + delay(300); + hanleQuitMenuEvent(); + } +} + +void joyStickDebugMenu(void) { + while (currentOptionPage == 1) { + printScreen(0, 14, "Debugging joystick.", true, false); + getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true); + u8g2.sendBuffer(); + delay(300); + printScreen(0, 14, "Debugging joystick..", true, false); + getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true); + u8g2.sendBuffer(); + delay(300); + printScreen(0, 14, "Debugging joystick...", true, false); + getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true); + u8g2.sendBuffer(); + delay(300); + printScreen(0, 14, "Debugging joystick....", true, false); + getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true); + u8g2.sendBuffer(); + delay(300); + hanleQuitMenuEvent(); + } +} + +CursorPos printOptionMenu(int lastX, int lastY) { + CursorPos pos = {lastX, lastY}; + + pos.y+=12; + u8g2.setCursor(pos.x, pos.y); + u8g2.print("1: Debug Joystick"); + if (selectOptionID == 1) { + u8g2.print(" [X]"); + } else { + u8g2.print(" [ ]"); + } + + pos.y+=12; + u8g2.setCursor(pos.x, pos.y); + u8g2.print("2: FM Broadcast"); + if (selectOptionID == 2) { + u8g2.print(" [X]"); + } else { + u8g2.print(" [ ]"); + } + return pos; +} + +void handleOptionSelectEvent() { + JoyStickStatus joyStatus = getJoyStickerStatus(JOY_X, JOY_Y, JOY_SW); + size_t lengthOfOptions = sizeof(optionIDList) / sizeof(optionIDList[0]); + + if (joyStatus.down) { + if (selectOptionID+1 <= lengthOfOptions) { + selectOptionID+=1; + } + } + else if (joyStatus.up) { + if (selectOptionID-1 >= 1) { + selectOptionID-=1; + } + } + + if (joyStatus.pressed) { + buttonPressCount+=1; + } + + // Do option page replace if button is pressed twice. + if (buttonPressCount>=2) { + buttonPressCount=0; + currentOptionPage=selectOptionID; + + // Reset + selectOptionID=1; + } +} + +void homeMenu(void) { + while (currentOptionPage==0) { + printScreen(0, 14, "Running_", true, false); + CursorPos currentPos = printOptionMenu(0, 14); + printScreen(currentPos.x, currentPos.y+24, "FM Brocaster v1.0", false, false); + u8g2.sendBuffer(); + delay(300); + + // Handle option select event while the joystick is moving (?) + handleOptionSelectEvent(); + + printScreen(0, 14, "Running ", true, false); + currentPos = printOptionMenu(0, 14); + printScreen(currentPos.x, currentPos.y+24, "FM Brocaster v1.0", false, false); + u8g2.sendBuffer(); + delay(300); + } +} + +void setup() { + // debug + Serial.begin(115200); + + // Initialize display + u8g2.begin(); + + // fm module + Wire.begin(); + Wire.setClock(100000); + + // scan i2c devices + byte error, address; + int nDevices; + + for(address = 1; address < 127; address++ ) { + Wire.beginTransmission(address); + error = Wire.endTransmission(); + if (error == 0) { + Serial.print("I2C device found at address 0x"); + if (address<16) Serial.print("0"); + Serial.println(address,HEX); + nDevices++; + } + } + if (nDevices == 0) Serial.println("No I2C devices found\n"); + + // Set font and en-able unicode support + u8g2.enableUTF8Print(); + u8g2.setFont(u8g2_font_6x12_tf); + + u8g2.clearBuffer(); + + // Joystick + pinMode(JOY_SW, INPUT_PULLUP); + analogReadResolution(12); + + // Set center position of the joystick + Serial.println("Correcting joystick... (Don't touch joystick when processing...)"); + + int rawX = analogRead(JOY_X); + int rawY = analogRead(JOY_Y); + int cx = 2048; + int cy = 2048; + + swapJoyStickVertical = true; + + // pcm5102 + // I2S.setPins( + // I2S_BCLK, + // I2S_LRCK, + // I2S_DOUT, + // -1, + // -1 + // ); + + // if (!I2S.begin( + // I2S_MODE_STD, + // SAMPLE_RATE, + // I2S_DATA_BIT_WIDTH_16BIT, + // I2S_SLOT_MODE_STEREO + // )) { + // Serial.println("Unable to init PCM5102!\n"); + // } else { + // Serial.println("Found PCM5102\n"); + // } + + Serial.println("Initialization Finished"); +} + +void loop(void) { + // static float phase = 0.0f; + + // int16_t sample = (int16_t)( + // sinf(phase) * AMPLITUDE + // ); + + // // Stereo: Left + Right + // int16_t stereo[2] = { + // sample, + // sample + // }; + + // I2S.write( + // (uint8_t *)stereo, + // sizeof(stereo) + // ); + + // phase += 2.0f * PI * FREQUENCY / SAMPLE_RATE; + + // if (phase >= 2.0f * PI) { + // phase -= 2.0f * PI; + // } + + if (currentOptionPage == 0) { + homeMenu(); + } + else if (currentOptionPage == 1) { + joyStickDebugMenu(); + } + else if (currentOptionPage == 2) { + fmBroadcast(); + } +} \ No newline at end of file diff --git a/FM_Brocaster_original b/FM_Brocaster_original new file mode 100644 index 0000000..8e5f6f3 --- /dev/null +++ b/FM_Brocaster_original @@ -0,0 +1,887 @@ +/* +ESP32 Frequency Modulation Broadcaster + +Some code is from my old game project but now is almost dead. + +Wei - 2026 + +You can do anything you want to this source code. BUT you MUST cite the original source of the code. +*/ +#include +#include +#include +#include +#include "KT0803.h" +#include +#include "Audio.h" + +#define VERSION "1.2" + +KT0803L fm(&Wire); +Audio audio; + +// ======================= Display ======================= +#define SCREEN_W 128 +#define SCREEN_H 64 + +#define OLED_MOSI 23 // SDA +#define OLED_CLK 18 // SCK +#define OLED_DC 27 +#define OLED_CS 5 +#define OLED_RST 14 // RES + +U8G2_SH1106_128X64_NONAME_F_4W_HW_SPI u8g2( + U8G2_R0, + OLED_CS, + OLED_DC, + OLED_RST +); + +// ======================= Joystick Pins ======================= +#define JOY_X 34 +#define JOY_Y 35 +#define JOY_SW 32 + +#define FM_SDA 21 +#define FM_SCL 22 + +// ======================= SD (microSD) Card ======================= +#define SD_MOSI 23 // sd reader share spi pin with the display +#define SD_MISO 19 +#define SD_SCK 18 +#define SD_CS 33 + +// ======================= PCM5102A DAC ======================= +#define I2S_BCLK 26 +#define I2S_LRCK 25 +#define I2S_DOUT 17 + +constexpr uint32_t SAMPLE_RATE = 44100; +constexpr float TONE_FREQ = 440.0f; + +I2SClass I2S; + +typedef struct CursorPos { + int x; + int y; +} CursorPos; + +void printScreen(int x, int y, const char* text, bool clear_screen, bool send_buffer) { + if (clear_screen) { + u8g2.clearBuffer(); + } + + u8g2.setCursor(x, y); + u8g2.print(text); + + if (send_buffer) { + u8g2.sendBuffer(); + } +} + +struct JoyStickStatus; + +// Change its value if your joystick's position is not same as you expected. +bool swapJoyStickVertical = false; +bool swapJoyStickHorizontal = false; + +JoyStickStatus getJoyStickerStatus(int pin_x, int pin_y, int pin_sw); + +struct JoyStickStatus { + bool up; + bool down; + bool left; + bool right; + bool pressed; +}; + +int readJoystickAxis(int pin) { + constexpr int SAMPLE_COUNT = 4; + + int sum = 0; + + for (int i = 0; i < SAMPLE_COUNT; i++) { + sum += analogRead(pin); + } + + return sum / SAMPLE_COUNT; +} + +JoyStickStatus getJoyStickerStatus(int pin_x, int pin_y, int pin_sw) { + JoyStickStatus status = {0}; + + constexpr int CENTER_LOW = 1500; + constexpr int CENTER_HIGH = 2600; + + int x = readJoystickAxis(pin_x); + int y = readJoystickAxis(pin_y); + bool pressed = (digitalRead(pin_sw) == LOW); + + status.up = x >= CENTER_LOW && x <= CENTER_HIGH && y < 500; + status.down = x >= CENTER_LOW && x <= CENTER_HIGH && y > 3800; + status.left = y >= CENTER_LOW && y <= CENTER_HIGH && x < 500; + status.right = y >= CENTER_LOW && y <= CENTER_HIGH && x > 3800; + + if (swapJoyStickVertical) { + // Wow, c++ have this cool thing + std::swap(status.up, status.down); + } + + if (swapJoyStickHorizontal) { + std::swap(status.left, status.right); + } + + status.pressed = pressed; + return status; +} + +void getJoyStickerDebugInfo(int start_x, int start_y, int pin_x, int pin_y, int pin_sw, bool print_in_serial) { + int x = readJoystickAxis(pin_x); + int y = readJoystickAxis(pin_y); + bool pressed = (digitalRead(pin_sw) == LOW); + + u8g2.setCursor(start_x, start_y); + u8g2.print("X: "); + u8g2.print(x); + + u8g2.setCursor(start_x, start_y + 12); + u8g2.print("Y: "); + u8g2.print(y); + + u8g2.setCursor(start_x, start_y + 24); + u8g2.print("SW: "); + u8g2.print(pressed ? "PRESSED" : "RELEASED"); + + JoyStickStatus status = getJoyStickerStatus(JOY_X, JOY_Y, JOY_SW); + + u8g2.setCursor(start_x, start_y + 36); + if (status.up) { + u8g2.print("Direction: UP"); + } + else if (status.down) { + u8g2.print("Direction: DOWN"); + } + else if (status.left) { + u8g2.print("Direction: LEFT"); + } + else if (status.right) { + u8g2.print("Direction: RIGHT"); + } + + if (print_in_serial) { + char buffer[100]; + snprintf(buffer, sizeof(buffer), "X:%d , Y:%d, SW:%s", x, y, pressed ? "PRESSED" : "RELEASED"); + Serial.println(buffer); + } +} + +// Audio +int dacVolume = 12; + +// Option & Menu +int selectOptionID = 1; +int currentOptionPage = 0; // 0: Home, 1: JoyStick Debug, 2: FM Broadcast, 3: PCM5102 Debug +const int optionIDList[] = {1,2,3,4,5}; +const char* menuNames[] = { + "Debug Joystick", + "FM Brocaster", + "Debug PCM5102", + "Volume", + "Play Music" +}; +int buttonPressCount = 0; +bool wasPressed = false; +unsigned long buttonPressTime = 0; + +float roundToOneDecimal(float num) { + return (int)(num * 10 + 0.5) / 10.0; +} + +void hanleQuitMenuEvent() { + JoyStickStatus joyStatus = getJoyStickerStatus(JOY_X, JOY_Y, JOY_SW); + + if (joyStatus.pressed && !wasPressed) { + // Start time record + buttonPressTime = millis(); + wasPressed = true; + } + + if (!joyStatus.pressed && wasPressed) { + // Quit menu if pressed time more than over 1.5 sec + unsigned long duration = millis() - buttonPressTime; + + if (duration >= 1000) { + currentOptionPage=0; + } + + wasPressed = false; + } +} + +void setFM(float freq, bool mute) { + fm.setUSA(); + fm.setFrequency(freq); + fm.setMute(mute); +} + +void fmBroadcast(void) { + float fm_freq = 98.1; + float old_freq = 98.1; + + if (!fm.begin(fm_freq, false)) { + Serial.println("FM Module KT0803 not found. Exiting..."); + printScreen(0, 14, "FM Module KT0803 not", true, false); + u8g2.setCursor(0, 26); + u8g2.print(" found. Exiting..."); + u8g2.sendBuffer(); + delay(3500); + currentOptionPage=0; + return; + } + + setFM(fm_freq, false); + + while (currentOptionPage == 2) { + int adc = analogRead(JOY_X); + int offset = adc - 2048; + + if (abs(offset) > 200) + { + float speed = offset / 2048.0f; + + fm_freq += speed * 0.1f; + fm_freq = roundToOneDecimal(fm_freq); + + // Max and min value check + if (fm_freq < 88.0) { + fm_freq = 88.0; + } + else if (fm_freq > 108.0) { + fm_freq = 108.0; + } + } + + if (old_freq != fm_freq) { + // Update frequency + setFM(fm_freq, false); + old_freq = fm_freq; + Serial.print("Frequency: "); + Serial.println(fm_freq); + } + + printScreen(0, 14, "Brocasting_", true, false); + u8g2.setCursor(0, 26); + u8g2.print("Frequency: "); + u8g2.print(fm.getFrequency()); + u8g2.setCursor(0, 38); + u8g2.print("Now you can leave"); + u8g2.setCursor(0, 50); + u8g2.print("this page."); + u8g2.sendBuffer(); + delay(300); + printScreen(0, 14, "Brocasting ", true, false); + u8g2.setCursor(0, 26); + u8g2.print("Frequency: "); + u8g2.print(fm.getFrequency()); + u8g2.setCursor(0, 38); + u8g2.print("Now you can leave"); + u8g2.setCursor(0, 50); + u8g2.print("this page."); + u8g2.sendBuffer(); + delay(300); + hanleQuitMenuEvent(); + } +} + +void joyStickDebugMenu(void) { + while (currentOptionPage == 1) { + printScreen(0, 14, "Debugging joystick.", true, false); + getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true); + u8g2.sendBuffer(); + delay(300); + printScreen(0, 14, "Debugging joystick..", true, false); + getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true); + u8g2.sendBuffer(); + delay(300); + printScreen(0, 14, "Debugging joystick...", true, false); + getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true); + u8g2.sendBuffer(); + delay(300); + printScreen(0, 14, "Debugging joystick....", true, false); + getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true); + u8g2.sendBuffer(); + delay(300); + hanleQuitMenuEvent(); + } +} + +CursorPos printOptionMenu(int lastX, int lastY, int availableHeight, int textSize) { + CursorPos pos = {lastX, lastY}; + + int optionCount = sizeof(menuNames) / sizeof(menuNames[0]); + int maxDisplayOptionCount = availableHeight / textSize; + int usedHeight = 0; + + // Below code is for scrolling menu support + int startIndex = selectOptionID - maxDisplayOptionCount / 2; + + // border check + if (startIndex < 0) { + startIndex = 0; + } + + if (startIndex + maxDisplayOptionCount > optionCount) { + startIndex = optionCount - maxDisplayOptionCount; + } + + if (startIndex < 0) { + startIndex = 0; + } + + int endIndex = min(startIndex + maxDisplayOptionCount, optionCount); + + for (int i = startIndex; i < endIndex; i++) { + pos.y += 12; + u8g2.setCursor(pos.x, pos.y); + + // Content + u8g2.print(i+1); + u8g2.print(": "); + u8g2.print(menuNames[i]); + + if (selectOptionID == i + 1) { + u8g2.print(" [X]"); + } else { + u8g2.print(" [ ]"); + } + } + + return pos; +} + +void handleOptionSelectEvent() { + JoyStickStatus joyStatus = getJoyStickerStatus(JOY_X, JOY_Y, JOY_SW); + size_t lengthOfOptions = sizeof(optionIDList) / sizeof(optionIDList[0]); + + if (joyStatus.down) { + if (selectOptionID+1 <= lengthOfOptions) { + selectOptionID+=1; + } + } + else if (joyStatus.up) { + if (selectOptionID-1 >= 1) { + selectOptionID-=1; + } + } + + if (joyStatus.pressed) { + buttonPressCount+=1; + } + + // Do option page replace if button is pressed twice. + if (buttonPressCount>=2) { + buttonPressCount=0; + currentOptionPage=selectOptionID; + + // Reset + selectOptionID=1; + } +} + +void printLogo(int x, int y) { + // Draw background + u8g2.setDrawColor(1); + u8g2.drawBox(x, y, 128 - x, 1); + + // Draw text + u8g2.setCursor(x + 2, y + 10); + + u8g2.print("FM Brocaster v"); + u8g2.print(VERSION); +} + +void homeMenu(void) { + bool frameMoment = false; + unsigned long lastDisplayUpdate = millis(); + unsigned long lastTickUpdate = millis(); + unsigned long lastAnimateUpdate = millis(); + unsigned long perFrameTime = 33; + unsigned long perTickTime = 500; + unsigned long perAnimateTime = 500; + + while (currentOptionPage == 0) { + unsigned long time = millis(); + if (time - lastTickUpdate >= perTickTime) { + lastTickUpdate = millis(); + handleOptionSelectEvent(); + } + + if (time - lastAnimateUpdate >= perAnimateTime) { + lastAnimateUpdate = millis(); + frameMoment = !frameMoment; + } + + if (time - lastDisplayUpdate >= perFrameTime) { + lastDisplayUpdate = millis(); + u8g2.clearBuffer(); + u8g2.setCursor(0, 14); + + if (frameMoment) { + u8g2.print("Running_"); + } else { + u8g2.print("Running "); + } + + CursorPos currentPos = printOptionMenu(0, 14, 36, 12); + printLogo(currentPos.x, currentPos.y + 2); + + u8g2.sendBuffer(); + } + + delay(1); + } +} + +void test_pcm5102(float sineFreq, float squareFreq, float triangleFreq) { + static float sinePhase = 0.0f; + static float squarePhase = 0.0f; + static float trianglePhase = 0.0f; + + constexpr size_t FRAMES = 256; + + int16_t samples[FRAMES * 2]; + + const float sineStep = TWO_PI * sineFreq / SAMPLE_RATE; + const float squareStep = TWO_PI * squareFreq / SAMPLE_RATE; + const float triangleStep = TWO_PI * triangleFreq / SAMPLE_RATE; + + for (size_t i = 0; i < FRAMES; i++) { + float sine = 0.0f; + float square = 0.0f; + float triangle = 0.0f; + + if (sineFreq > 0.0f) { + sine = sinf(sinePhase) * 2500.0f; + } + + if (squareFreq > 0.0f) { + square = (squarePhase < PI ? 1.0f : -1.0f) * 1000.0f; + } + + if (triangleFreq > 0.0f) { + triangle = + (trianglePhase < PI + ? (2.0f * trianglePhase / PI - 1.0f) + : (3.0f - 2.0f * trianglePhase / PI)) + * 1000.0f; + } + + float mixed = sine + square + triangle; + + mixed = constrain(mixed, -32768.0f, 32767.0f); + + int16_t sample = static_cast(mixed); + + samples[i * 2] = sample; + samples[i * 2 + 1] = sample; + + sinePhase += sineStep; + squarePhase += squareStep; + trianglePhase += triangleStep; + + if (sinePhase >= TWO_PI) + sinePhase -= TWO_PI; + + if (squarePhase >= TWO_PI) + squarePhase -= TWO_PI; + + if (trianglePhase >= TWO_PI) + trianglePhase -= TWO_PI; + } + + I2S.write( + reinterpret_cast(samples), + sizeof(samples) + ); +} + +void pcm5102DebugMenu(void) { + unsigned long lastDisplayUpdate = 0; + int frameMoment = 0; + float frequency = TONE_FREQ; + float squareFreq = 0.0f; + float triangleFreq = 0.0f; + + while (currentOptionPage == 3) { + test_pcm5102(frequency, squareFreq, triangleFreq); + + u8g2.clearBuffer(); + u8g2.setCursor(0, 14); + + int adc = readJoystickAxis(JOY_X); + int offset = adc - 2048; + + if (abs(offset) > 200) + { + float speed = offset / 2048.0f; + + frequency += speed * 0.1f; + frequency = roundToOneDecimal(frequency); + } + + int adcY = readJoystickAxis(JOY_Y); + offset = adcY - 2048; + + if (offset > 0) { + // For triangle + offset = abs(offset); + + float speed = offset / 2048.0f; + triangleFreq += speed * 0.1f; + triangleFreq = roundToOneDecimal(triangleFreq); + } else { + // For square + offset = abs(offset); + + float speed = offset / 2048.0f; + squareFreq += speed * 0.1f; + squareFreq = roundToOneDecimal(squareFreq); + } + + if (millis() - lastDisplayUpdate >= 300) { + lastDisplayUpdate = millis(); + + u8g2.clearBuffer(); + u8g2.setCursor(0, 14); + + switch (frameMoment) { + case 0: + u8g2.print("Debugging PCM5102."); + break; + + case 1: + u8g2.print("Debugging PCM5102.."); + break; + + case 2: + u8g2.print("Debugging PCM5102..."); + break; + + case 3: + u8g2.print("Debugging PCM5102...."); + break; + } + + u8g2.setCursor(0, 28); + u8g2.print("Sine Freq: "); + u8g2.print(frequency); + u8g2.setCursor(0, 40); + u8g2.print("Square Freq: "); + u8g2.print(squareFreq); + u8g2.setCursor(0, 52); + u8g2.print("Triangle Freq: "); + u8g2.print(triangleFreq); + + u8g2.sendBuffer(); + + frameMoment = (frameMoment + 1) % 4; + + hanleQuitMenuEvent(); + } + } +} + +void volumeMenu() { + static unsigned long lastVolumeUpdate = 0; + while (currentOptionPage == 4) { + if (millis() - lastVolumeUpdate >= 30) { + lastVolumeUpdate = millis(); + + JoyStickStatus status = getJoyStickerStatus(JOY_X, JOY_Y, JOY_SW); + int offset=0; + if (status.left) { + offset = 1; + } else if (status.right){ + offset = -1; + } + + dacVolume += offset; + + dacVolume = constrain(dacVolume, 0, 255); + + int barWidth = (dacVolume * 84) / 255; + + u8g2.firstPage(); + do { + u8g2.setCursor(0, 14); + u8g2.print("Volume"); + + u8g2.drawFrame(20, 30, 88, 12); + u8g2.drawBox(22, 32, barWidth, 8); + } while (u8g2.nextPage()); + + hanleQuitMenuEvent(); + delay(1); + } + } +} + +constexpr int MAX_MUSIC_FILES = 20; +constexpr int MAX_FILENAME_LEN = 48; + +char musicFiles[MAX_MUSIC_FILES][MAX_FILENAME_LEN]; +int musicFileCount = 0; +int selectedMusic = 0; + +void scanMusicFiles() { + musicFileCount = 0; + + File dir = SD.open("/music"); + + if (!dir || !dir.isDirectory()) { + return; + } + + File file = dir.openNextFile(); + + while (file && musicFileCount < MAX_MUSIC_FILES) { + if (!file.isDirectory()) { + const char* name = file.name(); + + const char* ext = strrchr(name, '.'); + + if (ext && strcasecmp(ext, ".mp3") == 0) { + strncpy( + musicFiles[musicFileCount], + name, + MAX_FILENAME_LEN - 1 + ); + + musicFiles[musicFileCount][MAX_FILENAME_LEN - 1] = '\0'; + + musicFileCount++; + } + } + + file.close(); + file = dir.openNextFile(); + } + + dir.close(); +} + +void handleMusicSelect() { + int y = readJoystickAxis(JOY_Y); + + if (y < 500) { + selectedMusic--; + + if (selectedMusic < 0) { + selectedMusic = musicFileCount - 1; + } + + delay(200); + } + + if (y > 3800) { + selectedMusic++; + + if (selectedMusic >= musicFileCount) { + selectedMusic = 0; + } + + delay(200); + } +} + +void drawMusicMenu() { + u8g2.firstPage(); + + do { + u8g2.setCursor(0, 10); + u8g2.print("Music"); + + if (musicFileCount == 0) { + u8g2.setCursor(0, 30); + u8g2.print("No MP3 files"); + continue; + } + + for (int row = -1; row <= 1; row++) { + int index = selectedMusic + row; + + if (index < 0 || index >= musicFileCount) { + continue; + } + + int y = 32 + row * 12; + + if (index == selectedMusic) { + u8g2.setCursor(0, y); + u8g2.print(">"); + } + + String name = musicFiles[index]; + + if (name.startsWith("/music/")) { + name.remove(0, 7); + } + + if (name.length() > 17) { + name = name.substring(0, 17); + } + + u8g2.setCursor(10, y); + u8g2.print(name); + } + + } while (u8g2.nextPage()); +} + +void musicMenu() { + scanMusicFiles(); + while (currentOptionPage == 5) { + handleMusicSelect(); + + drawMusicMenu(); + + if (digitalRead(JOY_SW) == LOW) { + if (musicFileCount > 0) { + Serial.print("Selected: "); + Serial.println(musicFiles[selectedMusic]); + audio.connecttoFS(SD, musicFiles[selectedMusic]); + } + + delay(200); + } + + hanleQuitMenuEvent(); + + delay(1); + } +} + +void setup() { + // debug + Serial.begin(115200); + + // Initialize display + u8g2.begin(); + u8g2.setContrast(255); + + // fm module + Wire.begin(); + Wire.setClock(100000); + + // ram check + if (psramFound()) { + Serial.println("PSRAM found!"); + Serial.printf("PSRAM size: %u bytes\n", ESP.getPsramSize()); + Serial.printf("Free PSRAM: %u bytes\n", ESP.getFreePsram()); + } else { + Serial.println("No PSRAM found."); + } + + // scan i2c devices + byte error, address; + int nDevices; + + for(address = 1; address < 127; address++ ) { + Wire.beginTransmission(address); + error = Wire.endTransmission(); + if (error == 0) { + Serial.print("I2C device found at address 0x"); + if (address<16) Serial.print("0"); + Serial.println(address,HEX); + nDevices++; + } + } + if (nDevices == 0) Serial.println("No I2C devices found\n"); + + // Set font and en-able unicode support + u8g2.enableUTF8Print(); + u8g2.setFont(u8g2_font_6x12_tf); + + u8g2.clearBuffer(); + + // Joystick + pinMode(JOY_SW, INPUT_PULLUP); + analogReadResolution(12); + + // Set center position of the joystick + Serial.println("Correcting joystick... (Don't touch joystick when processing...)"); + + int rawX = analogRead(JOY_X); + int rawY = analogRead(JOY_Y); + int cx = 2048; + int cy = 2048; + + // pcm5102 + I2S.setPins( + I2S_BCLK, + I2S_LRCK, + I2S_DOUT, + -1, + -1 + ); + audio.setPinout(I2S_BCLK, I2S_LRCK, I2S_DOUT); + audio.setVolume(dacVolume); + + if (!I2S.begin( + I2S_MODE_STD, + SAMPLE_RATE, + I2S_DATA_BIT_WIDTH_16BIT, + I2S_SLOT_MODE_STEREO + )) { + Serial.println("Unable to init PCM5102!\n"); + } else { + Serial.println("Found PCM5102\n"); + } + + // Init SPI bus for sd card reader + SPI.begin(SD_SCK, SD_MISO, SD_MOSI); + + // sd card + if (!SD.begin(SD_CS, SPI)) { + Serial.println("Unable to init SD card module. Did the SD module is installed?\n"); + return; + } else { + Serial.println("Found SD card module."); + + uint8_t cardType = SD.cardType(); + + if (cardType == CARD_NONE) { + Serial.println("No SD card detected."); + } else { + Serial.printf("SD Card Size: %llu MB\n", SD.cardSize() / (1024ULL * 1024ULL)); + scanMusicFiles(); + + for (int i = 0; i < musicFileCount; i++) { + Serial.println(musicFiles[i]); + } + } + } + + Serial.println("Initialization Finished"); +} + +void loop(void) { + if (currentOptionPage == 0) { + homeMenu(); + } + else if (currentOptionPage == 1) { + joyStickDebugMenu(); + } + else if (currentOptionPage == 2) { + fmBroadcast(); + } + else if (currentOptionPage == 3) { + pcm5102DebugMenu(); + } + else if (currentOptionPage == 4) { + volumeMenu(); + } + else if (currentOptionPage == 5) { + musicMenu(); + } +} \ No newline at end of file