diff --git a/README.md b/README.md index 0dee32c..c50eee8 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,11 @@ View this project on [CADLAB.io](https://cadlab.io/project/1281). ## Text-to-Speech Library for Arduino - checkout this project into your ``sketchbook/libraries`` folder -- requires an amplifier on the PWM output pin (see below) +- requires an amplifier on the PWM output pin (see below) +- alternative output via Stream or callback method - see [blog articles](http://programmablehardware.blogspot.ie/search/label/tts) + ## Supported Hardware - ATmega328-based Arduinos (e.g., Uno, Pro, Pro Mini, etc.): pins 3, 9, 10 @@ -43,3 +45,4 @@ and ARM processors with DAC (Teensy, Due) - Teensy [forum](https://forum.pjrc.com/threads/44587-TTS-(Text-to-Speech)-Library-Port) - separate port/hack for MBED ARM with DAC [repository](https://developer.mbed.org/users/manitou/code/tts/) - Hackaday article on [LM386 amplifiers](https://hackaday.com/2016/12/07/you-can-have-my-lm386s-when-you-pry-them-from-my-cold-dead-hands/) +- Example on how to use the [Output via Streams](https://www.pschatzmann.ch/home/2021/06/22/text-to-speach-in-arduino-using-tts/) \ No newline at end of file diff --git a/TTS.cpp b/TTS.cpp index 66d568f..53cd71b 100644 --- a/TTS.cpp +++ b/TTS.cpp @@ -16,7 +16,6 @@ #include "TTS.h" #include "english.h" -#include "sound.h" #define NUM_VOCAB sizeof(s_vocab)/sizeof(VOCAB) #define NUM_PHONEME sizeof(s_phonemes)/sizeof(PHONEME) @@ -29,6 +28,7 @@ static byte seed2; static char phonemes[128]; static char modifier[sizeof(phonemes)]; // must be same size as 'phonemes' + // Lookup user specified pitch changes static const byte PROGMEM PitchesP[] = { 1, 2, 4, 6, 8, 10, 13, 16 }; @@ -319,14 +319,14 @@ static byte random2(void) return seed0; } -static byte playTone(int pin, byte soundNum, byte soundPos, char pitch1, char pitch2, byte count, byte volume) +byte TTS::playTone(byte soundNum, byte soundPos, char pitch1, char pitch2, byte count, byte volume) { const byte *soundData = &SoundData[soundNum * 0x40]; while (count-- > 0) { byte s = pgm_read_byte(&soundData[soundPos & 0x3fu]); - sound(pin, s & volume); + sound_api->sound(s & volume); pause(pitch1); - sound(pin, (s >> 4) & volume); + sound_api->sound((s >> 4) & volume); pause(pitch2); soundPos++; @@ -334,24 +334,46 @@ static byte playTone(int pin, byte soundNum, byte soundPos, char pitch1, char pi return soundPos & 0x3fu; } -static void play(int pin, byte duration, byte soundNumber) +void TTS::play(byte duration, byte soundNumber) { while (duration--) - playTone(pin, soundNumber, random2(), 7, 7, 10, 15); + playTone(soundNumber, random2(), 7, 7, 10, 15); } /****************************************************************************** * User API ******************************************************************************/ + + TTS::TTS(int p) { - pin = p; + sound_api = new Sound(p); defaultPitch = 7; + stream_ptr = nullptr; #ifdef __AVR__ pinMode(pin, OUTPUT); #endif } +TTS::TTS(tts_data_callback_type cb, int len) { + sound_api = new SoundCallback(cb, this, len); + defaultPitch = 7; + stream_ptr = nullptr; +} + +/** + * @brief Construct a new TTS object which outputs the data to an Arduino Stream + * + * @param out + */ +TTS::TTS(Print &out, int bits_per_sample) { + stream_ptr = &out; + sound_api = new SoundCallback((tts_data_callback_type)TTS::stream_data_callback, this, 512); + defaultPitch = 7; + getInfo().bits_per_sample = bits_per_sample; +} + + /* * Speak a string of phonemes */ @@ -373,7 +395,7 @@ void TTS::sayPhonemes(const char *textp) if (phonemesToData(textp, s_phonemes)) { // phonemes has list of sound bytes - soundOn(pin); + sound_api->soundOn(); // initialise random number seed seed0 = 0xecu; @@ -435,7 +457,7 @@ void TTS::sayPhonemes(const char *textp) // Make a white noise sound! byte volume = (duration == 6) ? 15 : 1; // volume mask for (duration <<= 2; duration > 0; duration--) { - playTone(pin, sound1Num, random2(), 8, 12, 11, volume); + playTone(sound1Num, random2(), 8, 12, 11, volume); // Increase the volume if (++volume == 16) volume = 15; // full volume from now on @@ -502,11 +524,11 @@ void TTS::sayPhonemes(const char *textp) byte sound1End = min(sound1Stop, sound2Stop); if (sound1Stop) - soundPos = playTone(pin, sound1Num, soundPos, pitch1, pitch1, sound1End, 15); + soundPos = playTone(sound1Num, soundPos, pitch1, pitch1, sound1End, 15); // s18 if (sound2Stop != 0x40) { - soundPos = playTone(pin, sound2Num, soundPos, pitch2, pitch2, sound2Stop - sound1End, 15); + soundPos = playTone(sound2Num, soundPos, pitch2, pitch2, sound2Stop - sound1End, 15); } // s23 if (sound1Duration != 0xff && duration < byte2) { @@ -517,13 +539,13 @@ void TTS::sayPhonemes(const char *textp) } // Call any additional sound if (byte1 == -1) - play(pin, 3, 30); // make an 'f' sound + play(3, 30); // make an 'f' sound else if (byte1 == -2) - play(pin, 3, 29); // make an 's' sound + play(3, 29); // make an 's' sound else if (byte1 == -3) - play(pin, 3, 33); // make a 'th' sound + play(3, 33); // make a 'th' sound else if (byte1 == -4) - play(pin, 3, 27); // make a 'sh' sound + play(3, 27); // make a 'sh' sound } while (--duration); @@ -543,7 +565,7 @@ void TTS::sayPhonemes(const char *textp) delay2(25); } // next phoneme } - soundOff(pin); + sound_api->soundOff(); } /* @@ -556,3 +578,7 @@ void TTS::sayText(const char *original) if (textToPhonemes(original, s_vocab, text)) sayPhonemes(text); } + + + + diff --git a/TTS.h b/TTS.h index 67ec9ea..e306f6e 100644 --- a/TTS.h +++ b/TTS.h @@ -13,12 +13,56 @@ #ifndef _TTS_H_ #define _TTS_H_ +#include "sound.h" + +/** + * @brief TTS Output Information + * + */ +struct TTSInfo { + int channels = 1; + int sample_rate = 12000; + int bits_per_sample = 8; +}; + +/** + * @brief TTS API + * + */ class TTS { public: - + /** + * @brief Construct a new TTS object using the default pin + * + */ TTS(int pin); + + /** + * @brief Construct a new TTS object - Uses the Callback to provide the result + * + */ + TTS(tts_data_callback_type cb, int len=512); + + /** + * @brief Construct a new TTS object which outputs the data to an Arduino Stream + * + * @param out + */ + TTS(Print &out, int bits_per_sample=16); + + /** + * @brief Destroy the TTS object + * + */ + ~TTS(){ + if(sound_api!=nullptr) { + delete sound_api; + } + } + + /** * speaks a string of (english) text */ @@ -39,9 +83,47 @@ class TTS { */ byte getPitch(void) { return defaultPitch; } - private: + /** + * @brief Get additional output information + * + * @return TTSInfo + */ + static TTSInfo& getInfo() { + static TTSInfo info; + return info; + } + + // allow callback to access private fields + friend void stream_data_callback(void *vtts, int len, byte *data); + + protected: byte defaultPitch; - int pin; + BaseSound *sound_api = nullptr; + Print *stream_ptr = nullptr; + + void play(byte duration, byte soundNumber); + byte playTone(byte soundNum, byte soundPos, char pitch1, char pitch2, byte count, byte volume); + + // callback to write sound data to stream + static void stream_data_callback(void *vtts, int len, byte *data){ + TTS *tts = (TTS *) vtts; + TTSInfo *info = &getInfo(); + if (tts->stream_ptr!=nullptr && len>0 && data!=nullptr){ + if (info->bits_per_sample==8){ + tts->stream_ptr->write((const char*)data, len); + } else { + // convert 8 to 16 bits + for (int j=0;jstream_ptr->write((const char*)&sample16, 2); + } + } + } + } + }; + + #endif diff --git a/examples/Speak/Speak.ino b/examples/Speak/Speak.ino new file mode 100644 index 0000000..561ce18 --- /dev/null +++ b/examples/Speak/Speak.ino @@ -0,0 +1,22 @@ + +/** + * @brief Simple example which should work on most environments + */ + +#include "TTS.h" + + +// TTS tts(3); // with explicit pin +TTS tts; // with default pin + +void setup(){ + Serial.begin(115200); + // display pin + Serial.print("Using Pin "); + Serial.println(DEFAULT_PIN); +} + +void loop(){ + tts.sayText("hallo"); + delay(5000); +} \ No newline at end of file diff --git a/examples/SpeakWithCallback/SpeakWithCallback.ino b/examples/SpeakWithCallback/SpeakWithCallback.ino new file mode 100644 index 0000000..8628d95 --- /dev/null +++ b/examples/SpeakWithCallback/SpeakWithCallback.ino @@ -0,0 +1,22 @@ +/** + * @brief Example with Callback which just prints the result + * + */ +#include "TTS.h" + +void data_callback(int len, byte *data){ + for (int j=0;jpin = pin; +} + + +void Sound::soundOff() { #if defined(__AVR__) @@ -53,7 +59,7 @@ void soundOff(int pin) } //https://sites.google.com/site/qeewiki/books/avr-guide/pwm-on-the-atmega328 -void soundOn(int pin) +void Sound::soundOn() { #if defined(__AVR__) @@ -126,7 +132,7 @@ void soundOn(int pin) #endif } -void sound(int pin, byte b) +void Sound::sound(byte b) { // Update PWM volume b = (b & 15); @@ -195,4 +201,27 @@ void sound(int pin, byte b) analogWrite(pin, b*8); #endif + +} + +void SoundCallback::soundOff(){ + (*tts_callback)(tts, current_length, out_data); + memset(out_data,0, current_length); + current_length = 0; + active = false; +} + +void SoundCallback::soundOn() { + active = true; } + +void SoundCallback::sound(byte b) { + out_data[current_length++] = b ; + if (current_length>=max_length){ + if (tts_callback!=nullptr) { + (*tts_callback)(tts, current_length, out_data); + } + memset(out_data, 0, current_length); + current_length = 0; + } +} \ No newline at end of file diff --git a/sound.h b/sound.h index bd4b0a9..57c8077 100644 --- a/sound.h +++ b/sound.h @@ -1,10 +1,85 @@ #ifndef __SOUND_H__ #define __SOUND_H__ -void soundOff(int pin); -void soundOn(int pin); -void sound(int pin, byte b); - #define PWM_TOP (1200/2) +// Determine default PIN +#if defined(ESP32) +#define DEFAULT_PIN 25 +#elif defined(ESP8266) +#define DEFAULT_PIN 16 +#elif defined(__arm__) +#define DEFAULT_PIN DAC0 +#elif defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) +#define DEFAULT_PIN 44 +#elif defined(__AVR__) +#define DEFAULT_PIN 3 +#endif + +// Define callback data type +typedef void (*tts_data_callback_type)(void *tts, int len, byte *data); + +/** + * @brief Base Output Class + * + */ +class BaseSound { + public: + virtual ~BaseSound() {}; + virtual void soundOff() = 0; + virtual void soundOn() = 0; + virtual void sound(byte b) = 0; + +}; + +/** + * @brief Output to Pin + * + */ +class Sound : public BaseSound { + public: + Sound(); + Sound(int pin); + void soundOff(); + void soundOn(); + void sound(byte b); + + private: + int pin; + +}; + +/** + * @brief Output to Callback + * + */ +class SoundCallback : public BaseSound { + public: + SoundCallback(tts_data_callback_type callback, void *tts, int len=512){ + // allocate result array + this->tts = tts; + this->tts_callback = callback; + this->out_data = new byte[len+1]; + this->max_length = len; + } + + ~SoundCallback(){ + delete[] out_data; + } + + void soundOff(); + void soundOn(); + void sound(byte b); + + private: + void *tts; + tts_data_callback_type tts_callback = nullptr; + byte *out_data = nullptr; + int max_length = 0; + int current_length = 0; + bool active = false; + + +}; + #endif