For quite a long time I wanted to add TTS (text-to-speech) to my MCU applications and I tried quite few of them with more or less success always hitting a wall that either quality is not good or needed CPU power is too much.
However I recently found a very old TTS from ZX Spectrum (in the link is more info and also link to original tap file repository) that is really good and simple (just 801 Bytes of Z80 asm code). So I did it a try , disassembled it (extract the basic and asm from tap file by my own utilities and disassembled with YAZD) and port the result to C++ with complete success. It sound good on both PC and MCU with very little CPU power needed. It produces 1 bit digital sound.
Here is the C++ source code I made:
//---------------------------------------------------------------------------
//--- ZX Hlasovy program voicesoft 1985 -----------------------------------
//--- ported to C++ by Spektre ver: 1.001 -----------------------------------
//---------------------------------------------------------------------------
#ifndef _speech_h
#define _speech_h
//---------------------------------------------------------------------------
// API:
void sound_out(bool on); // you need to code this function (should add a sample to sound output)
void say_text(char *txt); // say null terminated text, "a'c'" -> "áè"
//---------------------------------------------------------------------------
// internals:
void say_char(char chr); // internal function for single character (do not use it !!!)
void say_wait(WORD ws); // internal wait (do not use it !!!)
//---------------------------------------------------------------------------
// vars:
bool _sound_on=false; // global state of the reproductor/sound output
//---------------------------------------------------------------------------
// config: (recomputed for 44100 Hz samplerate)
const static BYTE t_speed=5; // [samples] 1/(speech speed) (pitch)
const static WORD t_pause=183; // [samples] pause between chars
const static WORD t_space=2925; // [samples] pause ` `
const static WORD t_comma=5851; // [samples] pause `,`
//---------------------------------------------------------------------------
// tables:
const static BYTE tab_char0[52]= // 0..25 normal alphabet A..Z
{ // 26..51 diacritic alphabet A..Z
0x00,0x02,0x06,0x0a,0x0e,0x10,0x12,0x16,0x1a,0x1c,0x22,0x26,0x2a,0x2e,0x32,
0x34,0x38,0x42,0x48,0x4a,0x4e,0x50,0x50,0x56,0x1a,0x5c,0x64,0x66,0x70,0x74,
0x7a,0x7c,0xc2,0x84,0x86,0xc2,0xc2,0xc2,0x88,0x8c,0x92,0x94,0xc2,0x9e,0xa6,
0xa8,0xae,0xb0,0xc2,0xc2,0x86,0xbc
};
const static BYTE tab_char1[196]=
{
0x36,0x81,0x34,0x19,0x31,0xab,0x18,0x19,0x91,0xc3,0x34,0x19,0x31,0xe0,0x36,
0x84,0x92,0xe3,0x35,0x19,0x51,0x9c,0x31,0x31,0x34,0x96,0x36,0x87,0x33,0x3a,
0x32,0x3d,0x32,0xc0,0x18,0x19,0x51,0x9c,0x33,0x22,0x31,0xb1,0x31,0x31,0x36,
0xa5,0x31,0x31,0x36,0xa8,0x36,0x8a,0x18,0x19,0x31,0xab,0x18,0x19,0x51,0x1c,
0x34,0x31,0x32,0x34,0x32,0xb7,0x22,0x10,0x13,0x19,0x21,0xae,0x92,0xc3,0x18,
0x19,0x31,0xe0,0x36,0x8d,0x34,0x31,0x32,0x34,0x32,0xb7,0x18,0x19,0x71,0x1c,
0x92,0xc3,0x32,0x31,0x32,0x43,0x32,0x44,0x32,0xc5,0x3f,0x81,0x34,0x19,0x31,
0x2b,0x33,0x3a,0x32,0x3d,0x32,0xc0,0x18,0x19,0x91,0xd3,0x33,0x19,0x71,0x6d,
0x32,0x93,0x3e,0x84,0x92,0x63,0x33,0x3a,0x32,0x3d,0x32,0xc0,0x92,0xf3,0x3e,
0x87,0x31,0x31,0x36,0x25,0x31,0x31,0x35,0x25,0x32,0x93,0x3e,0x8a,0x18,0x19,
0x31,0x2b,0x33,0x3a,0x32,0x3d,0x32,0xc0,0x13,0x19,0x32,0x60,0x13,0x19,0x71,
0xdd,0x92,0xd3,0x18,0x19,0x71,0x6d,0x32,0x93,0x3e,0x8d,0x34,0x31,0x32,0x34,
0x32,0x37,0x33,0x3a,0x32,0x3d,0x32,0xc0,0x32,0x53,0x32,0x54,0x32,0xd5,0x1a,
0x99
};
const static BYTE tab_char2[262]=
{
0x1a,0x99,0xe1,0xc3,0xe1,0xc7,0x8f,0x0f,0xf8,0x03,0x0f,0x07,0xc1,0xe3,0xff,
0x40,0x17,0xff,0x00,0x03,0xf8,0x7c,0xc1,0xf1,0xf8,0x03,0xfe,0x00,0x7f,0xfc,
0x00,0x03,0xf8,0x0f,0x09,0xf1,0xfe,0x03,0xef,0x40,0x17,0xff,0x00,0x03,0xe1,
0x5c,0x35,0xc5,0xaa,0x35,0x00,0x00,0x00,0x00,0x00,0x00,0x3e,0x8e,0x38,0x73,
0xcf,0xf8,0x78,0xc3,0xdf,0x1c,0xf1,0xc7,0xfe,0x03,0xc0,0xff,0x00,0x00,0xff,
0xf8,0x00,0x7f,0xf8,0x03,0xff,0xf0,0x01,0xff,0xe0,0x03,0xaa,0xca,0x5a,0xd5,
0x21,0x3d,0xfe,0x1f,0xf8,0x00,0x00,0x1f,0xff,0xfc,0x20,0x00,0x00,0x03,0xff,
0xff,0x08,0x79,0x00,0x02,0xff,0xe1,0xc7,0x1f,0xe0,0x03,0xff,0xd0,0x01,0xff,
0xf0,0x03,0x7f,0x01,0xfa,0x5f,0xc0,0x07,0xf8,0x0f,0xc0,0xff,0x00,0x42,0xaa,
0xa5,0x55,0x5a,0xaa,0xaa,0x5a,0xa5,0x5a,0xaa,0x55,0x55,0xaa,0xaa,0xa5,0x55,
0xaa,0x5a,0xaa,0xa5,0x55,0xaa,0xaa,0xa5,0x55,0xaa,0xaa,0x55,0xa5,0xa5,0xaa,
0xa5,0xb7,0x66,0x6c,0xd8,0xf9,0xb3,0x6c,0xad,0x37,0x37,0x66,0xfc,0x9b,0x87,
0xf6,0xc0,0xd3,0xb6,0x60,0xf7,0xf7,0x3e,0x4d,0xfb,0xfe,0x5d,0xb7,0xde,0x46,
0xf6,0x96,0xb4,0x4f,0xaa,0xa9,0x55,0xaa,0xaa,0xa5,0x69,0x59,0x9a,0x6a,0x95,
0x55,0x95,0x55,0x6a,0xa5,0x55,0xa9,0x4d,0x66,0x6a,0x92,0xec,0xa5,0x55,0xd2,
0x96,0x55,0xa2,0xba,0xcd,0x00,0x66,0x99,0xcc,0x67,0x31,0x8e,0x66,0x39,0xa6,
0x6b,0x19,0x66,0x59,0xc6,0x71,0x09,0x67,0x19,0xcb,0x01,0x71,0xcc,0x73,0x19,
0x99,0xcc,0xc6,0x67,0x19,0x9a,0xc6,
};
const static BYTE tab_char3[5]={ 0x00,0x2e,0x5a,0x5e,0xfe };
//---------------------------------------------------------------------------
void say_text(char *txt)
{
WORD hl;
BYTE a,b,c;
for (b=0xBB,hl=0;;hl++) // process txt
{
a=b; // a,c char from last iteration
c=b;
if (!a) break; // end of txt
b=txt[hl]; // b actual char
if ((b>='a')&&(b<='z')) b=b+'A'-'a'; // must be uppercase
a=c;
if ((a>='A')&&(a<='Z'))
{
// handle diacritic
if (a!='C'){ a=b; if (a!='\'') a=c; else{ a=c; a+=0x1A; b=0xBB; }}
else{
a=b;
if (a=='H'){ a+=0x1A; b=0xBB; }
else{ if (a!='\'') a=c; else{ a=c; a+=0x1A; b=0xBB; }}
}
// syntetize sound
say_char(a);
continue;
}
if (a==',')say_wait(t_comma);
if (a==' ')say_wait(t_space);
}
}
//----------------------------------------------------------------------
void say_wait(WORD ws)
{
for (;ws;ws--) sound_out(_sound_on);
}
//----------------------------------------------------------------------
void say_char(char chr) // chr = < `A` , `Z`+26 >
{
WORD hl,hl0;
BYTE a,b,c,cy,cy0,ws;
hl=tab_char0[chr-'A'];
for (;;)
{
c =tab_char1[hl ]&0x0F;
c|=tab_char1[hl+1]&0x80;
for (;;)
{
a=tab_char1[hl];
a=(a>>5)&7;
cy=a&1;
hl0=hl;
if (a!=0)
{
b=tab_char3[a];
hl=hl0;
a=tab_char1[hl+1];
hl0=hl;
cy0=(a>>7)&1;
a=((a<<1)&254)|cy;
cy=cy0;
hl=a;
a=0x80;
for (;;)
{
_sound_on=(a&tab_char2[hl]);
for (ws=t_speed;ws;ws--) sound_out(_sound_on);
b--;
if (!b) break;
cy=a&1;
a=((a>>1)&127)|(cy<<7);
if (!cy) continue;
hl++;
}
}
a^=a;
say_wait(t_pause);
c--;
a=c&0x0F;
hl=hl0;
if (a==0) break;
}
cy0=(c>>7)&1;
a=((c<<1)&254)|cy;
cy=cy0;
if (cy) return;
hl+=2;
}
}
//---------------------------------------------------------------------------
#endif
//---------------------------------------------------------------------------
This works perfectly however I would like to understand how the sound is synthetized. I can not make any sense of it... is it some sort of compression of samples or uses formant filter to synthetize sound or combines them or its something else?
So I want to dissect the say_char
function to make sense/meaning of the tab_char?[]
LUT tables.
[Edit2] thanks to Edward new more C/C++ like version
I rearranged the tables and added a lot of comment info to be more didactical and possible to tweak:
//---------------------------------------------------------------------------
//--- ZX Hlasovy program voicesoft 1985 -----------------------------------
//--- ported to C++ by Spektre ver: 2.001 -----------------------------------
//---------------------------------------------------------------------------
#ifndef _speech_h
#define _speech_h
//---------------------------------------------------------------------------
// API:
void sound_out(bool on); // you need to code this function (should add a sample to sound output)
void say_text(char *txt); // say null terminated text, "a'c'" -> "áč"
//---------------------------------------------------------------------------
// internals:
void say_char(char chr); // internal function for single character (do not use it !!!)
void say_wait(WORD ws); // internal wait (do not use it !!!)
//---------------------------------------------------------------------------
// vars:
bool _sound_on=false; // global state of the reproductor/sound output
//---------------------------------------------------------------------------
// config: (recomputed for 44100 Hz samplerate)
const static BYTE t_speed=5; // [samples] 1/(speech speed) (pitch)
const static WORD t_pause=183; // [samples] pause between chars
const static WORD t_space=2925; // [samples] pause ` `
const static WORD t_comma=5851; // [samples] pause `,`
//---------------------------------------------------------------------------
// point to RLE encoded character sound (RLE_ix)
const static BYTE tab_char[52]=
{
// A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
0, 1, 3, 5, 7, 8, 9,11,13,14,17,19,21,23,25,26,28,33,36,37,39,40,40,43,13,46,
// A' B' C' D' E' F' G' H' I' J' K' L' M' N' O' P' Q' R' S' T' U' V' W' X' Y' Z'
50,51,56,58,61,62,97,66,67,97,97,97,68,70,73,74,97,79,83,84,87,88,97,97,67,94,
};
// RLE encoded character sounds
const static WORD tab_RLE[98]=
{
// 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
// end -----num------ ------------PCM_ix-----------
// ix char
0x9804, // 0 A
0x103D,0x8473, // 1 B
0x203C,0x84AB, // 3 C
0x103D,0x8524, // 5 D
0x980B, // 7 E
0x892B, // 8 F
0x143D,0x8444, // 9 G
0x0481,0x9035, // 11 H
0x9812, // 13 I,Y
0x0C96,0x089D,0x88A4, // 14 J
0x203C,0x8444, // 17 K
0x0C5E,0x8481, // 19 L
0x0481,0x9865, // 21 M
0x0481,0x986C, // 23 N
0x9819, // 25 O
0x203C,0x8473, // 26 P
0x203C,0x0444,0x1081,0x0888,0x888F, // 28 Q
0x0827,0x0C3C,0x847A, // 33 R
0x88AB, // 36 S
0x203C,0x8524, // 37 T
0x9820, // 39 U
0x1081,0x0888,0x888F, // 40 V,W
0x203C,0x0451,0x88AB, // 43 X
0x0881,0x08CC,0x08D3,0x88DA, // 46 Z
0xBC04, // 50 A'
0x103D,0x0473,0x0C96,0x089D,0x88A4, // 51 B' *
0x203C,0x84E1, // 56 C'
0x0C3D,0x054C,0x882E, // 58 D'
0xB80B, // 61 E'
0x092B,0x0C96,0x089D,0x88A4, // 62 F' *
0x8959, // 66 CH,H'
0xB812, // 67 I',Y'
0x0481,0x1865, // 68 M' overlap with N' *
0x0481,0x1465,0x882E, // 70 N' overlap with M'
0xB819, // 73 O'
0x203C,0x0473,0x0C96,0x089D,0x88A4, // 74 P' *
0x0C3C,0x0924,0x0C3C,0x8517, // 79 R'
0x88E1, // 83 S'
0x203C,0x054C,0x882E, // 84 T'
0xB820, // 87 U'
0x1081,0x0888,0x088F,0x0C96,0x089D,0x88A4, // 88 V',W' *
0x0902,0x0909,0x8910, // 94 Z'
0xA83C, // 97 G',J',K',L',Q',X',W' (no sound)
// missing: Ľ/Ĺ,Ř/Ŕ,Ú/ˇU,ô,ä,é/ě
// accent?: B',F',M',P',V'
// nosound: G',J',K',L',Q',X',W'
};
// formant sounds sampled as 1bit PCM
const static BYTE tab_PCM[]=
{
// bits,1bit PCM samples // ix,sample in binary
24,0x1A,0x99,0xE1, // 0,000110101001100111100001
46,0xC3,0xE1,0xC7,0x8F,0x0F,0xF8, // 4,110000111110000111000111100011110000111111111000
46,0x03,0x0F,0x07,0xC1,0xE3,0xFF, // 11,000000110000111100000111110000011110001111111111
46,0x40,0x17,0xFF,0x00,0x03,0xF8, // 18,010000000001011111111111000000000000001111111000
46,0x7C,0xC1,0xF1,0xF8,0x03,0xFE, // 25,011111001100000111110001111110000000001111111110
46,0x00,0x7F,0xFC,0x00,0x03,0xF8, // 32,000000000111111111111100000000000000001111111000
46,0x0F,0x09,0xF1,0xFE,0x03,0xEF, // 39,000011110000100111110001111111100000001111101111
46,0x40,0x17,0xFF,0x00,0x03,0xE1, // 46,010000000001011111111111000000000000001111100001
46,0x5C,0x35,0xC5,0xAA,0x35,0x00, // 53,010111000011010111000101101010100011010100000000
0, // 60,
46,0x00,0x00,0x00,0x00,0x00,0x3E, // 61,000000000000000000000000000000000000000000111110
90,0x3E,0x8E,0x38,0x73,0xCF,0xF8,0x78,0xC3, // 68,0011111010001110001110000111001111001111111110000111100011000011
0xDF,0x1C,0xF1,0xC7, // 11011111000111001111000111000111
94,0x8E,0x38,0x73,0xCF,0xF8,0x78,0xC3,0xDF, // 81,1000111000111000011100111100111111111000011110001100001111011111
0x1C,0xF1,0xC7,0xFE, // 00011100111100011100011111111110
46,0x03,0xC0,0xFF,0x00,0x00,0xFF, // 94,000000111100000011111111000000000000000011111111
46,0xF8,0x00,0x7F,0xF8,0x03,0xFF, // 101,111110000000000001111111111110000000001111111111
46,0xF0,0x01,0xFF,0xE0,0x03,0xAA, // 108,111100000000000111111111111000000000001110101010
46,0xCA,0x5A,0xD5,0x21,0x3D,0xFE, // 115,110010100101101011010101001000010011110111111110
46,0x1F,0xF8,0x00,0x00,0x1F,0xFF, // 122,000111111111100000000000000000000001111111111111
46,0xFC,0x20,0x00,0x00,0x03,0xFF, // 129,111111000010000000000000000000000000001111111111
46,0xFF,0x08,0x79,0x00,0x02,0xFF, // 136,111111110000100001111001000000000000001011111111
46,0xE1,0xC7,0x1F,0xE0,0x03,0xFF, // 143,111000011100011100011111111000000000001111111111
46,0xD0,0x01,0xFF,0xF0,0x03,0x7F, // 150,110100000000000111111111111100000000001101111111
46,0x01,0xFA,0x5F,0xC0,0x07,0xF8, // 157,000000011111101001011111110000000000011111111000
46,0x0F,0xC0,0xFF,0x00,0x42,0xAA, // 164,000011111100000011111111000000000100001010101010
254,0xAA,0xA5,0x55,0x5A,0xAA,0xAA,0x5A,0xA5, // 171,1010101010100101010101010101101010101010101010100101101010100101
0x5A,0xAA,0x55,0x55,0xAA,0xAA,0xA5,0x55, // 0101101010101010010101010101010110101010101010101010010101010101
0xAA,0x5A,0xAA,0xA5,0x55,0xAA,0xAA,0xA5, // 1010101001011010101010101010010101010101101010101010101010100101
0x55,0xAA,0xAA,0x55,0xA5,0xA5,0xAA,0xA5, // 0101010110101010101010100101010110100101101001011010101010100101
46,0xA5,0x55,0x5A,0xAA,0xAA,0x5A, // 204,101001010101010101011010101010101010101001011010
46,0x5A,0xAA,0xAA,0x5A,0xA5,0x5A, // 211,010110101010101010101010010110101010010101011010
46,0xAA,0x5A,0xA5,0x5A,0xAA,0x55, // 218,101010100101101010100101010110101010101001010101
254,0xB7,0x66,0x6C,0xD8,0xF9,0xB3,0x6C,0xAD, // 225,1011011101100110011011001101100011111001101100110110110010101101
0x37,0x37,0x66,0xFC,0x9B,0x87,0xF6,0xC0, // 0011011100110111011001101111110010011011100001111111011011000000
0xD3,0xB6,0x60,0xF7,0xF7,0x3E,0x4D,0xFB, // 1101001110110110011000001111011111110111001111100100110111111011
0xFE,0x5D,0xB7,0xDE,0x46,0xF6,0x96,0xB4, // 1111111001011101101101111101111001000110111101101001011010110100
46,0x66,0x6C,0xD8,0xF9,0xB3,0x6C, // 258,011001100110110011011000111110011011001101101100
46,0xD8,0xF9,0xB3,0x6C,0xAD,0x37, // 265,110110001111100110110011011011001010110100110111
46,0xB3,0x6C,0xAD,0x37,0x37,0x66, // 272,101100110110110010101101001101110011011101100110
94,0x3E,0x4D,0xFB,0xFE,0x5D,0xB7,0xDE,0x46, // 279,0011111001001101111110111111111001011101101101111101111001000110
0xF6,0x96,0xB4,0x4F, // 11110110100101101011010001001111
46,0xDE,0x46,0xF6,0x96,0xB4,0x4F, // 292,110111100100011011110110100101101011010001001111
254,0x4F,0xAA,0xA9,0x55,0xAA,0xAA,0xA5,0x69, // 299,0100111110101010101010010101010110101010101010101010010101101001
0x59,0x9A,0x6A,0x95,0x55,0x95,0x55,0x6A, // 0101100110011010011010101001010101010101100101010101010101101010
0xA5,0x55,0xA9,0x4D,0x66,0x6A,0x92,0xEC, // 1010010101010101101010010100110101100110011010101001001011101100
0xA5,0x55,0xD2,0x96,0x55,0xA2,0xBA,0xCD, // 1010010101010101110100101001011001010101101000101011101011001101
94,0x6A,0x92,0xEC,0xA5,0x55,0xD2,0x96,0x55, // 332,0110101010010010111011001010010101010101110100101001011001010101
0xA2,0xBA,0xCD,0x00, // 10100010101110101100110100000000
254,0x00,0x66,0x99,0xCC,0x67,0x31,0x8E,0x66, // 345,0000000001100110100110011100110001100111001100011000111001100110
0x39,0xA6,0x6B,0x19,0x66,0x59,0xC6,0x71, // 0011100110100110011010110001100101100110010110011100011001110001
0x09,0x67,0x19,0xCB,0x01,0x71,0xCC,0x73, // 0000100101100111000110011100101100000001011100011100110001110011
0x19,0x99,0xCC,0xC6,0x67,0x19,0x9A,0xC6, // 0001100110011001110011001100011001100111000110011001101011000110
};
//---------------------------------------------------------------------------
void say_text(char *txt)
{
int i;
char a0,a1;
for (a1=0xBB,i=0;a1;i++) // process txt
{
a0=a1; a1=txt[i]; // a0,a1 are last,actual char
if ((a1>='a')&&(a1<='z')) a1+='A'-'a'; // a..z -> A..Z
if ((a0=='C')&&(a1=='H')){ a0='H'; a1='\''; } // CH -> H'
if ((a0>='A')&&(a0<='Z'))
{
if (a1=='\''){ a0+=0x1A; a1=0xBB; } // handle diacritic
say_char(a0); // syntetize sound
continue;
}
if (a0==',') say_wait(t_comma);
if (a0==' ') say_wait(t_space);
}
}
//----------------------------------------------------------------------
void say_wait(WORD ws)
{
for (;ws;ws--) sound_out(_sound_on);
}
//----------------------------------------------------------------------
void say_char(char chr) // chr = < `A` , `Z`+26 >
{
WORD a;
BYTE ws,pcm;
int i,j,e,num,pcm_ix,bits;
i=tab_char[chr-'A'];
for (e=1;e;i++)
{
a=tab_RLE[i];
e =!(a &0x8000);
num = (a>>10)&0x001F;
pcm_ix= a &0x03FF;
for (;num;num--)
{
for (j=pcm_ix,bits=tab_PCM[j],j++;bits;j++)
for (pcm=tab_PCM[j],a=0x80;(bits)&&(a);a>>=1,bits--)
for (_sound_on=(a&pcm),ws=t_speed;ws;ws--)
sound_out(_sound_on);
say_wait(t_pause);
}
}
}
//---------------------------------------------------------------------------
#endif
//---------------------------------------------------------------------------