Einfachen (aber qualitativ hochwertigen) TTS-Motor verstehen / umkehren

Dec 15 2020

Lange Zeit wollte ich meinen MCU-Anwendungen TTS (Text-to-Speech) hinzufügen, und ich habe einige davon mit mehr oder weniger Erfolg ausprobiert, wobei ich immer auf eine Wand gestoßen bin, bei der entweder die Qualität nicht gut ist oder die benötigte CPU-Leistung zu hoch ist .

Allerdings habe ich kürzlich ein sehr altes TTS von ZX Spectrum gefunden (im Link finden Sie weitere Informationen und auch einen Link zum ursprünglichen Tap-Datei-Repository), das wirklich gut und einfach ist (nur 801 Byte Z80-ASM-Code). Also habe ich es versucht, es zerlegt (die Basis- und ASM-Datei von meinen eigenen Dienstprogrammen aus der Tap-Datei extrahieren und mit YAZD zerlegen) und das Ergebnis mit vollem Erfolg nach C ++ portiert. Es klingt sowohl auf dem PC als auch auf der MCU gut, da nur sehr wenig CPU-Leistung benötigt wird. Es wird 1 Bit digitaler Sound erzeugt.

Hier ist der C ++ - Quellcode, den ich erstellt habe:

//---------------------------------------------------------------------------
//---  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
//---------------------------------------------------------------------------

Dies funktioniert perfekt, aber ich würde gerne verstehen, wie der Klang synthetisiert wird. Ich kann es nicht verstehen ... ist es eine Art Komprimierung von Samples oder verwendet es einen Formantenfilter, um den Klang zu synthetisieren oder sie zu kombinieren oder etwas anderes?

Ich möchte also die say_charFunktion zerlegen , um den Sinn / die Bedeutung der tab_char?[]LUT-Tabellen zu verstehen .

[Edit2] dank Edward neuer C / C ++ ähnlicher Version

Ich habe die Tabellen neu angeordnet und viele Kommentarinformationen hinzugefügt, um didaktischer und optimierbarer zu sein:

//---------------------------------------------------------------------------
//---  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
//---------------------------------------------------------------------------

Antworten

5 Edward Dec 16 2020 at 02:33

Ich kann die Rede aus dem Hlasový-Programm überhaupt nicht wirklich verstehen , aber vielleicht ist sie für Ihre Bedürfnisse geeignet.

Ich habe keine spezifischen Kenntnisse über diese spezielle Software, aber basierend auf dem Zeitpunkt der Veröffentlichung und der Größe handelt es sich zweifellos um ein formantbasiertes System. Die typische Software (auf den 8-Bit-Computern dieses Jahrgangs) verwendete eine Text-zu-Phonem- und dann eine Phonem-zu-Formanten-Konvertierung.

Ein etwas größeres, aber verständlicheres System aus dieser Zeit war "SAM" oder "Software Automated Mouth", das jetzt jemand auf Javascript portiert hat . Folgen Sie den Links von dort, um mehr zu erfahren, einschließlich des rückentwickelten C-Codes.

Der Autor dieser Software aus den frühen 1980er Jahren, Mark Barton, wurde kürzlich interviewt und bietet einige Einblicke in diese Software.

Dieses Programm

Hier ist eine weitere Analyse Ihrer rückentwickelten Software. Ich werde Ihnen sagen, wie ich es gemacht habe und das Ergebnis zeigen. Zuerst begann ich, die innerste Schleife zu betrachten und sie nacheinander neu zu schreiben, wobei ich das Ergebnis jedes Mal testete, um sicherzustellen, dass sie bei jedem Schritt identische Ergebnisse lieferte. Dann wiederholte ich das im Wesentlichen für immer größere Teile der Funktion. Ich habe auch Variablen umbenannt und hinzugefügt, damit sie besser widerspiegeln, wie die Software sie tatsächlich verwendet. Während der Z80 in den Registern, die er verwenden kann (und was diese Register können), begrenzt ist, gibt es in C ++ nicht dieselbe Einschränkung, daher wird der Code aus Gründen der Übersichtlichkeit neu geschrieben.

say_char ()

void say_char(char chr)         // chr =  < `A` , `Z`+26 >
{
    const Chain *chain = &chain_sequence[chain_start[chr - 'A']];
    for (BYTE c=0; (c & 0x80) == 0; ++chain) {
        // count is in low four bits of c, end flag is high bit
        for (c = chain->copies_and_end(); c & 0xf; --c) {
            BYTE a = chain->numbits_lookup();
            if (a != 0) {
                BYTE bitcount = num_bits[a];
                BYTE bitloc = chain->start_index();

                // bitcount is the number of bits to emit
                // starting with the MSB of sound_bits[bitloc]
                for ( ;bitcount; ++bitloc) {
                    for (BYTE mask = 0x80; mask; mask >>= 1) {
                        _sound_on = (mask & sound_bits[bitloc]);
                        for (BYTE ws = t_speed; ws; ws--)
                            sound_out(_sound_on);
                        if (--bitcount == 0)
                            break;
                    }
                }
            }
            say_wait(t_pause);
        }
    }
}

Hier ist die Erklärung. Zuerst habe ich die Strukturen umbenannt:

tab_char0 --> chain_start
tab_char1 --> chain_sequence
tab_char2 --> sound_bits
tab_char3 --> num_bits

Dann habe ich das geändert chain_sequence, um stattdessen eine Zwei-Byte-C ++ - Struktur zu verwenden. Die Definition lautet wie folgt:

struct Chain {
        // bits: 7    6    5    4    3    2    1    0
    BYTE a;  //  m2   m1   c0   -    l3   l2   l1   l0
    BYTE b;  // end | c7   c6   c5   c4   c3   c2   c1

    bool end() const { return b & 0x80; }
    BYTE copies() const { return a & 0x0F; }
    BYTE start_index() const { return ((b & 0x7f) << 1) | ((a & 0x20) >> 5); }
    BYTE copies_and_end() const {
        return (a & 0x0F) | (b & 0x80);
    }
    BYTE numbits_lookup() const {
        return (a >> 5) & 7;
    }
    friend std::ostream& operator<<(std::ostream& out, const Chain& ch) {
        return out 
            << "copies = " << unsigned(ch.copies())
            << ", start_index = " << unsigned(ch.start_index())
            << ", numbits_lookup = " << unsigned(ch.numbits_lookup())
            << ", end = " << std::boolalpha << bool(ch.b & 0x80)
            << ", useless = " << bool(ch.a & 0x10);
    }
};

Aufgrund dieser Änderung musste ich das ändern chain_start Tabelle , um jeden Eintrag zu halbieren.

Wie es funktioniert

Für jeden Buchstaben beginnt der Code mit einer Suche in der chain_startTabelle. Das ist ein Index in der chain_sequenceTabelle. Wenn wir die ersten drei Einträge in dieser Tabelle auswählen, sehen sie folgendermaßen aus:

const static Chain chain_sequence[98] = {
    /* A = 0 */ { 0x36, 0x81, },
    /* B = 1 */ { 0x34, 0x19, }, { 0x31, 0xab, },
    /* C = 3 */ { 0x18, 0x19, }, { 0x91, 0xc3, },

Jedes von diesen ist eine Kettensequenz, wobei das letzte Element mit dem hohen Bit des zweiten Bytesatzes identifiziert wird. Für den Buchstaben 'A' bedeutet dies:

copies = 6, start_index = 3, numbits_lookup = 1, end = true 

Dies bedeutet dann, dass der Code sechs Kopien eines Bitmusters erstellt. Jede Kopie endet mit t_pausenull Bits. Für die Anfangsbits jeder Kopie verwendet der Code den numbits_lookupWert, um die gewünschte Länge im 5-Byte-Format nachzuschlagennum_bits . Für 'A' ist die Suche also 1 und das entspricht 0x2e = 46, aber die Art und Weise, wie der Code geschrieben wird, entspricht tatsächlich einem tatsächlich emittierten Bit weniger oder in diesem Fall 45.

Als nächstes wird das start_indexals Index verwendet sound_bits. Jedes Byte in der Tabelle wird dann beginnend mit dem höchstwertigen Bit jedes Bytes getaktet. In diesem Fall entsprechen also Index 3 und eine Länge von 45 Bit diesen Einträgen in der Tabelle:

0xc3 0xe1 0xc7 0x8f, 0x0f, 0xf8

1100 0011  1110 0001  1100 0111  1000 1111  0000 1111  1111 10xx

Die letzten beiden mit xx gekennzeichneten Bits werden nicht verwendet. Dies hat zur Folge, dass die Ausgabe sechs Kopien davon entspricht:

1100001111100001110001111000111100001111111110
... followed by `t_pause` 0 bits

Kommentar

Übersetzungsfehler

Es gibt einen Fehler im Code. Wenn Sie genau hinschauen, eines der Elemente in dem, was ich anrufeChain nicht verwendet (Bit 4 des ersten Bytes), aber eines der anderen Bits wird zweimal verwendet (Bit 5 des ersten Bytes).

In der Tat habe ich den ursprünglichen Z80-Code zerlegt und Folgendes gefunden:

add hl,de       ; cy = 0 (can't overflow)
ld b,(hl)       ; b = bitlen[a];
pop hl          ;
inc hl          ;
ld a,(hl)       ; a = chain_sequence[hl + 1]
dec hl          ;
push hl         ;
rla             ; the carry shifted in is always zero
ld de,sound_bits    ; point to bit table
ld l,a          ;
ld h,000h       ;
add hl,de       ; hl = sound_bits[a]
ld a,080h       ; start with mask = 0x80

Ihr Code scheint zu implizieren, dass das Übertragsbit gesetzt ist, wenn Sie das aufrufen, was ich beschriftet habe, start_index()und es ist, aber näher an der relevanten rlaAnweisung, die das sound_bitsIndexbyte erstellt, ist das Übertragsbit garantiert Null. Der Add-Befehl kann, wie oben erwähnt, nicht überlaufen und löscht so das Übertragsbit. Keiner der Befehle von dort zum rlaBefehl ändert das Übertragsbit, so dass es an diesem Punkt Null ist.

Andere Beobachtungen

Auch die ersten drei Bytes der sound_bits Arrays scheinen nicht verwendet zu werden.

Es scheint nicht viele überlappende Daten zu geben, aber es könnte sein. Die Kettenfolge für einen der Buchstaben wird wiederverwendet. Ich habe nicht daran gearbeitet, die hier verwendeten diakritischen Zeichen zu dekodieren, aber wenn die zweiten 26 Buchstaben mit A 'bis Z' bezeichnet sind, beginnt der für M 'bei Index 68 und enthält 5 Kettensegmente. Die für N 'verwendet die letzten drei dieser Segmente.

Auch für kurze und lange Versionen desselben Vokals wie A und A '(A mit čárka bedeutet auf Tschechisch einen langen Vokal) wiederholt der aktuelle Code das Ketten-Token, jedoch nur mit einer längeren Sequenz. Es könnte möglich sein, sie zu kombinieren und ein einzelnes Bit-Flag zu verwenden, um einen Vokal anzuzeigen.

Auf einem 16-Bit-Computer könnte dies durch Umstrukturierung der Daten wesentlich effizienter gestaltet werden. Es kann auch so geändert werden, dass es auf einem eingebetteten System ereignisgesteuert ist. Dies könnte beispielsweise durch einen Timer-Interrupt unterbrochen werden. Oder man könnte eine Warteschlange mit Samples erstellen und diese mithilfe der DMA-Übertragung an einen Lautsprecher ausstempeln.

Physik

Dabei wird die niedrigste Frequenz über eine Folge von Bits (mindestens 45) gefolgt von t_pauseNullen erzeugt. Die höheren Frequenzen werden innerhalb der führenden Bitmuster in jeder Kopie erzeugt. Wie erwartet handelt es sich hierbei um einen formantbasierten Synthesizer mit relativ geringer Auflösung.