Files
FM_Brocaster/FM_Brocaster_original
T
wei 448a3f7fd6 Add PCM5102 Synthesizer and Play Music (mp3) support.
The original source is FM_Brocaster_original.
2026-08-24 00:02:05 +08:00

887 lines
20 KiB
Plaintext

/*
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 <SPI.h>
#include <SD.h>
#include <U8g2lib.h>
#include <Wire.h>
#include "KT0803.h"
#include <ESP_I2S.h>
#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<int16_t>(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<uint8_t *>(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();
}
}