#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#include "SPIFFS.h"
#else
#include <ESP8266WiFi.h>
#endif
#include "AudioFileSourceICYStream.h"
#include "AudioFileSourceBuffer.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2SNoDAC.h"
#include "AudioOutputI2S.h"
#include "AudioFileSourceSPIFFS.h"
#include "AudioFileSourceID3.h"
#include <ArduinoJson.h>
//SPIFFSに入れておく効果音。地震警報用なので2ベルと1ベルの効果音を用意
#define EFFECT_TWO_BELL "/0004.mp3"
#define EFFECT_ONE_BELL "/0001.mp3"
//Wifiの接続SSIDとパス
const char* ssid = "ssid";
const char* password = "pass";
const String msg = "緊急地震速報、予報。函館、震度2。最大震度、震度5強。マグニチュード、5.5。震源、宮城県沖。最終報待ちです";
//SoundOfTextのURL
const char *host = "https://api.soundoftext.com/sounds";
StaticJsonDocument<1024> json_request;
char buffer[512];
const char* root_ca= \
"-----BEGIN CERTIFICATE-----\n" \
"MIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw\n" \
"TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n" \
"cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw\n" \
"WhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg\n" \
"RW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\n" \
"AoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP\n" \
"R5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx\n" \
"sxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm\n" \
"NHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg\n" \
"Z3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG\n" \
"/kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC\n" \
"AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB\n" \
"Af8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA\n" \
"FHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw\n" \
"AoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw\n" \
"Oi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB\n" \
"gt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W\n" \
"PTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl\n" \
"ikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz\n" \
"CkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm\n" \
"lJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4\n" \
"avAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2\n" \
"yJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O\n" \
"yK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids\n" \
"hCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+\n" \
"HlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv\n" \
"MldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX\n" \
"nLRbwHOoq7hHwg==\n" \
"-----END CERTIFICATE-----\n";
bool tts_flag = false;
String location = "";
bool isPlaying = false;
String effect_mp3 = "";
String tts_text = "";
int tts_state = 0; // 0:待機 1:mp3url生成リクエスト中 2:再生中
AudioGeneratorMP3 *mp3;
AudioFileSourceICYStream *file;
AudioFileSourceBuffer *buff;
//AudioOutputI2SNoDAC *out;
AudioOutputI2S *out;
AudioFileSourceSPIFFS *file_effect;
AudioFileSourceID3 *id3_effect;
//プロトタイプ宣言
String requestCreateTTS(String text);
String isPreparedTTSMp3(String id,String& location);
bool playEffect();
void subProcess(void * pvParameters);
// Called when a metadata event occurs (i.e. an ID3 tag, an ICY block, etc.
void MDCallback(void *cbData, const char *type, bool isUnicode, const char *string)
{
/*
const char *ptr = reinterpret_cast<const char *>(cbData);
(void) isUnicode; // Punt this ball for now
// Note that the type and string may be in PROGMEM, so copy them to RAM for printf
char s1[32], s2[64];
strncpy_P(s1, type, sizeof(s1));
s1[sizeof(s1)-1]=0;
strncpy_P(s2, string, sizeof(s2));
s2[sizeof(s2)-1]=0;
Serial.printf("METADATA(%s) '%s' = '%s'\n", ptr, s1, s2);
Serial.flush();
*/
}
// Called when there's a warning or error (like a buffer underflow or decode hiccup)
void StatusCallback(void *cbData, int code, const char *string)
{
/*
const char *ptr = reinterpret_cast<const char *>(cbData);
// Note that the string may be in PROGMEM, so copy it to RAM for printf
char s1[64];
strncpy_P(s1, string, sizeof(s1));
s1[sizeof(s1)-1]=0;
Serial.printf("STATUS(%s) '%d' = '%s'\n", ptr, code, s1);
Serial.flush();
*/
}
void initTTS(){
/*
out = new AudioOutputI2SNoDAC();
out->SetPinout(26,25,5); // bclkPin = 26 wclkPin = 25 doutPin = 5
out->SetGain(3.999); // <4.0
out->SetRate(44100);
out->SetBitsPerSample(16);
out->SetChannels(1);
*/
out = new AudioOutputI2S();
out->SetPinout(26,25,15); // bclkPin = 26 wclkPin = 25 doutPin = 15
out->SetGain(0.03); // <4.0 0.15Def
out->SetRate(44100);
out->SetBitsPerSample(16);
out->SetChannels(1);
xTaskCreatePinnedToCore(subProcess, "subProcess", 8192, NULL, 25, NULL, 0); //Core0でもCore1でも優先度も適宜変更
}
void speak_text(String text,String effect_f){
//再生していれば停止をする
isPlaying = false;
//tts_state = 0 になるまで待機
Serial.print("[TTS] Wait tts_state reset");
while(tts_state != 0){
Serial.print(".");
delay(100);
}
Serial.println("");
Serial.println("[TTS] tts_state reset ok");
//効果音と発話内容をセット
effect_mp3 = effect_f;
tts_text = text;
//SoundOfTestへリクエスト開始
tts_state = 1;
}
bool playEffect(){
file_effect = new AudioFileSourceSPIFFS(effect_mp3.c_str());
id3_effect = new AudioFileSourceID3(file_effect);
mp3 = new AudioGeneratorMP3();
mp3->begin(id3_effect, out);
delay(50);
}
//戻り値 Pedding:生成中 Done:完了=>locationにmp3のurl Error:生成エラー
// https://soundoftext.com/docs参照
String isPreparedTTSMp3(String id,String& location){
String ret = "";
String url = String(host) + "/" + id;
HTTPClient http;
http.begin(url,root_ca);
int status_code = http.GET();
//Serial.print("status_code = ");
//Serial.println(status_code);
if( status_code == 200 ){
Stream* resp = http.getStreamPtr();
DynamicJsonDocument json_response(255);
deserializeJson(json_response, *resp);
ret = (const char*)json_response["status"];
if(json_response.containsKey("location")){
location = (const char*)json_response["location"];
}else{
location = "";
}
//serializeJson(json_response, Serial);
//Serial.println("");
}
http.end();
return ret;
}
//TTSのidを生成。空文字列ならサーバーエラーか生成エラー(適宜リトライ)
String requestCreateTTS(String text){
String ret = "";
//postで渡すjson生成
json_request.clear();
json_request["engine"] = "Google";
JsonObject obj = json_request.createNestedObject("data");
obj["text"] = text;
obj["voice"] = "ja-JP";
//obj["voice"] = "en-US";
//serializeJson(json_request, Serial);
//Serial.println("");
serializeJson(json_request, buffer, sizeof(buffer));
HTTPClient http;
http.begin(host,root_ca);
http.addHeader("Content-Type", "application/json");
int status_code = http.POST((uint8_t*)buffer, strlen(buffer));
//Serial.print("status_code = ");
//Serial.println(status_code);
if( status_code == 200 ){
Stream* resp = http.getStreamPtr();
DynamicJsonDocument json_response(255);
deserializeJson(json_response, *resp);
if(json_response.containsKey("id")){
ret = (const char*)json_response["id"];
}
//serializeJson(json_response, Serial);
//Serial.println("");
}
http.end();
return ret;
}
int createMp3FromSoundText(){
isPlaying = true;
String id = "";
int counter = 0;
while(id.length()<=0) {
id = requestCreateTTS(tts_text);
yield();
if(counter >= 5 ){
Serial.println("[requestCreateTTS] request over 5 retry");
return 0;
}else{
counter++;
}
}
Serial.print("[createMp3FromSoundText] success create mp3url id : ");
Serial.println(id);
if(!isPlaying) return 0;
String ret;
do{
ret = isPreparedTTSMp3(id,location);
Serial.print("ret = ");
Serial.println(ret);
} while ( ret.compareTo("Pending") == 0 );
//Done以外ならエラー
if( ret.compareTo("Done") != 0 ){
Serial.print("[isPreparedTTSMp3] error : ret = ");
Serial.println(ret);
return 0;
}
//https -> httpへ(Esp8266Audioはhttpしかダメなため)
location.replace("https", "http");
Serial.print("[createMp3FromSoundText] success create mp3url : ");
Serial.println(location);
if(isPlaying){
//effect_mp3の指定がなければ効果音なしでTTS再生
Serial.println("[TTS] start TTS play");
if(effect_mp3.compareTo("") == 0){
tts_flag = true;
file = new AudioFileSourceICYStream(location.c_str());
file->RegisterMetadataCB(MDCallback, (void*)"ICY");
buff = new AudioFileSourceBuffer(file, 2048);
buff->RegisterStatusCB(StatusCallback, (void*)"buffer");
mp3 = new AudioGeneratorMP3();
mp3->RegisterStatusCB(StatusCallback, (void*)"mp3");
mp3->begin(buff, out);
delay(100);
}else{
//effect_mp3の効果音再生後、TTS再生
tts_flag = false;
playEffect();
}
return 2;
}else{
Serial.println("[TTS] cancel before TTS play");
return 0;
}
}
void subProcess(void * pvParameters) {
while (1) {
if(tts_state == 0){
// 0 待機状態
}else if(tts_state == 1){
// 1 SoundTextからURLを生成
tts_state = createMp3FromSoundText();
}else if(tts_state == 2){
// 2 効果音とTTS再生
if ( mp3 != NULL ){
if (mp3->isRunning()) {
if(!isPlaying){
mp3->stop();
Serial.println("[TTS] cancel TTS");
}else if(!mp3->loop()){
mp3->stop();
}
} else {
delete mp3;
mp3 = NULL;
if(!tts_flag){
//効果音の再生完了
//Serial.println("tts_flag false");
file_effect->close();
delete file_effect;
delete id3_effect;
file_effect = NULL;
id3_effect = NULL;
//再生がキャンセルされてなければ次にTTSを再生
if(!isPlaying){
tts_flag = false;
tts_state = 0;
Serial.println("[TTS] finish TTS play (only effect)");
}else{
file = new AudioFileSourceICYStream(location.c_str());
file->RegisterMetadataCB(MDCallback, (void*)"ICY");
buff = new AudioFileSourceBuffer(file, 2048);
buff->RegisterStatusCB(StatusCallback, (void*)"buffer");
mp3 = new AudioGeneratorMP3();
mp3->RegisterStatusCB(StatusCallback, (void*)"mp3");
mp3->begin(buff, out);
tts_flag = true;
delay(100);
}
}else{
//TTSが再生完了
buff->close();
file->close();
delete buff;
delete file;
buff = NULL;
file = NULL;
//初期待機状態に戻す
tts_flag = false;
isPlaying = false;
tts_state = 0;
Serial.println("[TTS] finish TTS");
}
}
}
}
delay(1);
}
}
void setup()
{
Serial.begin(115200);
delay(1000);
Serial.println("Connecting to WiFi");
WiFi.disconnect();
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
Serial.println("...Connecting to WiFi");
delay(1000);
}
Serial.println("Connected");
//SPIFFS初期化
SPIFFS.begin();
//tts初期化
initTTS();
//発話(効果音EFFECT_TWO_BELL後にTTS)
speak_text(msg,EFFECT_TWO_BELL);
}
void loop(){
}