理解/逆向简单(但质量好)的 TTS 引擎

逆向工程 C++ 声音的
2021-07-04 02:50:15

很长一段时间以来,我想将 TTS(文本到语音)添加到我的 MCU 应用程序中,但我尝试了其中的一些,或多或少都取得了成功,但总是碰壁,要么质量不好,要么需要的 CPU 功率太多.

然而,我最近从 ZX Spectrum找到了一个非常旧的 TTS(链接中提供了更多信息,还链接到原始的 tap 文件存储库),它非常好且简单(只有 801 字节的 Z80 asm 代码)。所以我试了一下,反汇编了它(通过我自己的实用程序从 tap 文件中提取基本和 asm 并用 YAZD 反汇编)并将结果移植到 C++ 并完全成功。它在 PC 和 MCU 上听起来都不错,只需要很少的 CPU 功率。它产生 1 位数字声音。

这是我制作的 C++ 源代码:

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

这非常有效,但是我想了解声音是如何合成的。我无法理解它......它是某种样本压缩还是使用共振峰滤波器来合成声音或组合它们或其他东西?

所以我想剖析这个say_char函数来理解tab_char?[]LUT 表的意义

[Edit2] 感谢 Edward 推出了更多类似 C/C++ 的版本

我重新排列了表格并添加了很多评论信息,以使其更具教学意义并且可以进行调整:

//---------------------------------------------------------------------------
//---  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
//---------------------------------------------------------------------------
1个回答

我完全听不懂Hlasový 节目的演讲,但也许它适合您的需求。

我对这个特定软件没有任何具体了解,但根据发布时间和大小,它几乎无疑是一个基于共振峰的系统。典型的软件(在那个年代的 8 位计算机上)使用文本到音素,然后是音素到共振峰的转换。

那个时代一个更大但更易于理解的系统是“SAM”或“软件自动嘴”,现在有人已经移植到 Javascript按照那里的链接阅读更多内容,包括逆向工程 C 代码。

1980 年代早期该软件的作者 Mark Ba​​rton 实际上最近接受了采访,并提供了对该软件的一些见解。

这个节目

这是对逆向工程软件的进一步分析。我会告诉你我是如何做到的,并展示了结果。首先,我开始查看最内部的循环并连续重写它,每次测试结果以确保它在每一步产生相同的结果。然后我基本上对函数的越来越大的部分重复了这一点。我还重命名并添加了变量,以使它们更好地反映软件实际使用它们的方式。虽然 Z80 在它可以使用的寄存器(以及这些寄存器可以做什么)方面受到限制,但我们在 C++ 中没有同样的限制,因此为了清晰起见,重新编写了代码。

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);
        }
    }
}

这是解释。首先,我重命名了结构:

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

然后我修改了chain_sequence使用两字节的 C++ 结构。定义是这样的:

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);
    }
};

由于这一变化,我不得不修改chain_start表格以将每个条目减半。

怎么运行的

对于每个字母,代码从在chain_start表中查找开始那是chain_sequence表中的索引如果我们选择该表中的前三个条目,它们看起来像这样:

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

每一个都是一个链序列,最后一个项目用第二个字节集的高位标识。对于字母“A”,它翻译为:

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

这意味着代码创建了一个位模式的六个副本。每个副本以t_pause零位结束对于每个副本的起始位,代码使用该numbits_lookup值在 5-byte 中查找所需的长度num_bits因此,对于“A”,查找为 1,对应于 0x2e = 46,但代码的编写方式实际上对应于实际发出的位少一个,或者在这种情况下为 45。

接下来,它将start_index用作索引sound_bits然后表中的每个字节从每个字节的最高有效位开始计时。所以在这种情况下,索引 3 和 45 位的长度对应于表中的这些条目:

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

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

最后两位,标记为 xx 未使用。所以这样做的效果是输出对应于这个的六个副本:

1100001111100001110001111000111100001111111110
... followed by `t_pause` 0 bits

评论

翻译错误

代码中有一个错误。如果你仔细观察,我所说的其中一个位Chain没有使用(第一个字节的第 4 位),但其他位中的一个被使用了两次(第一个字节的第 5 位)。

确实,我反汇编了原来的Z80代码,发现了这个:

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

您的代码似乎暗示在调用我标记的内容时设置了进位位start_index(),但更接近rla创建sound_bits索引字节的相关指令,进位位保证为零。如上所述,加法指令不能溢出,因此清除进位位。从那里到rla指令的指令都没有改变进位位,因此在该点处为零。

其他观察

此外,sound_bits数组的前三个字节似乎未使用。

似乎没有很多重叠的数据,但可能有。重复使用其中一个字母的链式序列。我还没有对此处使用的实际变音符号进行解码,但如果将后 26 个字母指定为 A' 到 Z',则表示 M' 的字母从索引 68 开始并包含 5 个链段。N' 的一个使用这些段的最后三个。

同样对于相同元音的短和长版本,例如 A 和 A'(带有 čárka 的 A 在捷克语中表示长元音),当前代码重复链标记,但只是更长的序列。可以将它们组合起来并使用单个位标志来表示元音。

在 16 位机器上,这可以通过重组数据来提高效率。它也可以修改为在嵌入式系统上由事件驱动。例如,这可能是由定时器中断驱动的。或者可以创建一个样本队列并使用 DMA 传输将它们计时到扬声器。

物理

这样做是通过一系列位(最少 45 个)后跟t_pause来创建最低频率较高的频率是在每个副本的前导位模式中创建的。正如预期的那样,这是一个分辨率相对较低的基于共振峰的合成器。