プログラムを参考までに記載しました。
自由に使ってください。
(ArduinoIDEを使ってマイコンに書き込みます)
//MP3 VOICE CLOCK(wifiで時刻自動取得)
//2026.1.26 konan0119
#include <WiFi.h>
#include <time.h>
#include <SPI.h>
#include <SD.h>
#include <RTClib.h>
#include <AudioFileSourceSD.h>
#include <AudioGeneratorMP3.h>
#include <AudioOutputI2S.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// ===== RTC / MP3 =====
RTC_DS3231 rtc;
AudioGeneratorMP3 *mp3 = nullptr;
AudioFileSourceSD *file = nullptr;
AudioOutputI2S *out = nullptr;
const int SD_CS = 5;
unsigned long lastPlayTime = 0;
unsigned long lastAlarmTime = 0; // ★ アラーム後の時報抑制用
int sw;
const int VISIBLE_LINES = 4; // OLED に表示できる行数
int startIndex = 1; // 表示開始位置(1〜ALARM_SOUND_COUNT)
// ===== WiFi / NTP =====
const char* ssid = "your ssid";
const char* password = "your password";
const char* ntpServer = "ntp.nict.jp";
const long gmtOffset_sec = 9 * 3600;
const int daylightOffset_sec = 0;
// ===== 曜日表示 =====
const char* weekJP[7] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
// ===== アラーム設定 =====
int alarmHour = 7;//初期時刻
int alarmMinute = 30;//初期分
bool alarmEnabled = true;
int alarmSound = 1;
const int ALARM_SOUND_COUNT = 8;//アラームMP3ファイル保存数
// ===== 音量設定 =====
float chimeVolume = 0.4; // 時報音量
float alarmVolume = 0.4; // アラーム音量
// ===== モード =====
enum Mode {
NORMAL,
SET_HOUR,
SET_MIN,
SET_ONOFF,
SET_SOUND_LIST,
SET_CHIME_VOL,
SET_ALARM_VOL
};
Mode mode = NORMAL;
// ===== ボタン =====
#define BTN_MODE 32
#define BTN_UP 33
bool btnModePressed() { return digitalRead(BTN_MODE) == LOW; }
bool btnUpPressed() { return digitalRead(BTN_UP) == LOW; }
// ===== 太字1文字描画 =====
void drawBoldCharFixed(int x, int y, char c) {
oled.setCursor(x, y);
oled.print(c);
oled.setCursor(x + 1, y);
oled.print(c);
}
// ===== 時・分(大きく太字)=====
void drawHourMinute(int x, int y, int hour, int minute) {
oled.setTextSize(3);
oled.setTextColor(SSD1306_WHITE);
char buf[6];
sprintf(buf, "%02d:%02d", hour, minute);
int w = 20;
int cursorX = x;
for (int i = 0; i < 5; i++) {
drawBoldCharFixed(cursorX, y, buf[i]);
cursorX += w;
}
}
// ===== 秒(小さく)=====
void drawSecondSmall(int x, int y, int second) {
oled.setTextSize(2);
oled.setTextColor(SSD1306_WHITE);
char buf[3];
sprintf(buf, "%02d", second);
oled.setCursor(x, y + 6);
oled.print(buf);
}
// ===== NTP → RTC =====
void syncRTCfromNTP() {
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
struct tm timeinfo;
if (getLocalTime(&timeinfo)) {
rtc.adjust(DateTime(
timeinfo.tm_year + 1900,
timeinfo.tm_mon + 1,
timeinfo.tm_mday,
timeinfo.tm_hour,
timeinfo.tm_min,
timeinfo.tm_sec
));
}
}
//MP3 の再生時間(秒)を取得する関数
int getMP3Duration(String path) {
if (!SD.exists(path)) return 0;
File f = SD.open(path);
if (!f) return 0;
// MP3ヘッダを探す
while (f.available()) {
uint8_t b1 = f.read();
if (b1 == 0xFF) {
uint8_t b2 = f.read();
if ((b2 & 0xE0) == 0xE0) {
// MPEG1 Layer3 の場合
uint8_t b3 = f.read();
uint8_t b4 = f.read();
int bitrateIndex = (b3 >> 4) & 0x0F;
int sampleRateIndex = (b3 >> 2) & 0x03;
static const int bitrates[] = {
0, 32, 40, 48, 56, 64, 80, 96,
112, 128, 160, 192, 224, 256, 320, 0
};
static const int samplerates[] = {
44100, 48000, 32000, 0
};
int bitrate = bitrates[bitrateIndex] * 1000;
int samplerate = samplerates[sampleRateIndex];
if (bitrate == 0 || samplerate == 0) break;
// ファイルサイズから再生時間を計算
int fileSize = f.size();
int durationSec = fileSize * 8 / bitrate;
f.close();
return durationSec;
}
}
}
f.close();
return 0;
}
// ===== MP3アラーム再生用 =====
void playMP3(String path) {
if (!SD.exists(path)) return;
int duration = getMP3Duration(path);
if (duration <= 0) duration = 5; // 安全値
unsigned long timeout = duration * 1000 + 500; // 余裕を持たせる
file = new AudioFileSourceSD(path.c_str());
mp3 = new AudioGeneratorMP3();
if (!mp3->begin(file, out)) {
delete file; file = nullptr;
delete mp3; mp3 = nullptr;
return;
}
unsigned long start = millis();
while (mp3->isRunning()) {
mp3->loop();
if (millis() - start > timeout) break;
}
mp3->stop();
delete file; file = nullptr;
delete mp3; mp3 = nullptr;
}
// ===== MP3時報再生用 =====
void playMP32(String path) {
if (!SD.exists(path)) return;
file = new AudioFileSourceSD(path.c_str());
mp3 = new AudioGeneratorMP3();
if (!mp3->begin(file, out)) {
delete file; file = nullptr;
delete mp3; mp3 = nullptr;
return;
}
unsigned long start = millis();
unsigned long timeout = 0;
switch (sw) {
case 1:
timeout = 1200;
break;
case 2:
timeout = 5000;//アラーム設定時の再生時間
break;
}
Serial.println(timeout);
Serial.println("");
while (mp3->isRunning()) {
mp3->loop();
if (millis() - start > timeout) break;
}
mp3->stop();
delete file; file = nullptr;
delete mp3; mp3 = nullptr;
}
// ===== 設定画面 =====
void drawSettingScreen() {
oled.clearDisplay();
oled.setTextColor(SSD1306_WHITE);
oled.setTextSize(2);
if (mode == SET_HOUR) {
oled.setCursor(0, 0);
oled.print("Alarm Hour");
oled.setCursor(0, 30);
oled.printf("%02d", alarmHour);
}
else if (mode == SET_MIN) {
oled.setCursor(0, 0);
oled.print("Alarm Min");
oled.setCursor(0, 30);
oled.printf("%02d", alarmMinute);
}
else if (mode == SET_ONOFF) {
oled.setCursor(0, 0);
oled.print("Alarm");
oled.setCursor(30, 15);
oled.print("Swich");
oled.setCursor(0, 45);
oled.print(alarmEnabled ? "ON" : "OFF");
}
else if (mode == SET_SOUND_LIST) {
// 選択位置に応じて startIndex を調整
if (alarmSound < startIndex) {
startIndex = alarmSound;
} else if (alarmSound >= startIndex + VISIBLE_LINES) {
startIndex = alarmSound - VISIBLE_LINES + 1;
}
oled.clearDisplay();
oled.setTextSize(1);
oled.setCursor(0, 0);
oled.print("Select Alarm Sound");
// 表示するのは startIndex から最大 VISIBLE_LINES 行
for (int i = 0; i < VISIBLE_LINES; i++) {
int index = startIndex + i;
if (index > ALARM_SOUND_COUNT) break;
oled.setCursor(0, 15 + i * 12);
if (index == alarmSound) oled.print("> ");
else oled.print(" ");
oled.printf("%d: alarm%d.mp3", index, index);
}
oled.display();
}
else if (mode == SET_CHIME_VOL) {
oled.setTextSize(2);
oled.setCursor(0, 0);
oled.print("Chime Vol");
oled.setCursor(0, 30);
oled.printf("%.1f", chimeVolume);
}
else if (mode == SET_ALARM_VOL) {
oled.setTextSize(2);
oled.setCursor(0, 0);
oled.print("Alarm Vol");
oled.setCursor(0, 30);
oled.printf("%.1f", alarmVolume);
}
oled.display();
}
// ===== セットアップ =====
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
pinMode(BTN_MODE, INPUT_PULLUP);
pinMode(BTN_UP, INPUT_PULLUP);
oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
oled.clearDisplay();
oled.display();
WiFi.begin(ssid, password);
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED) {
if (millis() - start > 15000) break; // ★ WiFiタイムアウト
delay(200);
}
rtc.begin();
if (WiFi.status() == WL_CONNECTED) {
syncRTCfromNTP();
}
SD.begin(SD_CS);
out = new AudioOutputI2S();
out->SetGain(0.4);
}
// ===== メインループ =====
void loop() {
DateTime now = rtc.now();
int year = now.year();
int month = now.month();
int day = now.day();
int hour = now.hour();
int minute = now.minute();
int second = now.second();
int wday = now.dayOfTheWeek();
// ===== 設定モード =====
if (mode != NORMAL) {
drawSettingScreen();
if (btnUpPressed()) {
if (mode == SET_HOUR) alarmHour = (alarmHour + 1) % 24;
else if (mode == SET_MIN) alarmMinute = (alarmMinute + 1) % 60;
else if (mode == SET_ONOFF) alarmEnabled = !alarmEnabled;
else if (mode == SET_SOUND_LIST) {
alarmSound++;
if (alarmSound > ALARM_SOUND_COUNT) alarmSound = 1;
sw=2;
out->SetGain(alarmVolume);
String file = "/alarm/alarm" + String(alarmSound) + ".mp3";
playMP32(file);
}
else if (mode == SET_CHIME_VOL) {
chimeVolume += 0.1;
if (chimeVolume > 1.0) chimeVolume = 0.1;
}
else if (mode == SET_ALARM_VOL) {
alarmVolume += 0.1;
if (alarmVolume > 1.0) alarmVolume = 0.1;
}
delay(200);
}
if (btnModePressed()) {
if (mode == SET_HOUR) mode = SET_MIN;
else if (mode == SET_MIN) mode = SET_ONOFF;
else if (mode == SET_ONOFF) mode = SET_SOUND_LIST;
else if (mode == SET_SOUND_LIST) mode = SET_CHIME_VOL;
else if (mode == SET_CHIME_VOL) mode = SET_ALARM_VOL;
else if (mode == SET_ALARM_VOL) mode = NORMAL;
delay(300);
}
return;
}
// ===== 通常画面 =====
if (btnModePressed()) {
mode = SET_HOUR;
delay(300);
}
oled.clearDisplay();
oled.setTextColor(SSD1306_WHITE);
oled.setTextSize(2);
oled.setCursor(0, 0);
oled.printf("%04d/%02d/%02d (%s)", year, month, day, weekJP[wday]);
drawHourMinute(0, 32, hour, minute);
drawSecondSmall(100, 32, second);
oled.setTextSize(1);
oled.setCursor(0, 55);
oled.printf("Alarm Set:%02d:%02d %s S%d ",
alarmHour, alarmMinute,
alarmEnabled ? "ON" : "OFF",
alarmSound );
oled.display();
// ===== アラーム後5秒は時報を無効化 =====
bool skipChime = (millis() - lastAlarmTime < 5000);
// ===== 時報 =====
if (!skipChime &&
minute % 10 == 0 &&
second == 0 &&
millis() - lastPlayTime > 10000) {
out->SetGain(chimeVolume);
lastPlayTime = millis();
sw=1;
if (minute == 0) {
playMP32("/voice/jihou1.mp3");
playMP32("/voice/hh" + String(hour) + ".mp3");
} else {
playMP32("/voice/jihou2.mp3");
playMP32("/voice/h" + String(hour) + ".mp3");
playMP32("/voice/m" + String(minute) + ".mp3");
}
}
// ===== アラーム発動 =====
if (alarmEnabled &&
hour == alarmHour &&
minute == alarmMinute &&
second == 0) {
out->SetGain(alarmVolume);
String file = "/alarm/alarm" + String(alarmSound) + ".mp3";
playMP3(file);
lastAlarmTime = millis(); // ★ 時報抑制開始
}
delay(200);
}
