TTS 真人发音 SYN6288 模块

通常合成语音技术被称作 TTS (Text To Speech),这样的功能在报站,排队叫号等等场合有着广泛的应用。最近我入手了一个SYN6288的TTS模块,价格50元送喇叭。

image001

具体的说明可以在芯片手册中找到,对于我们来说,最直接的用法就是播放语音。推荐刚拿到模块的时候使用USB串口直接对模块发送下面的数据可以测试是否正常。
image002

从上面也能看出,命令的构成。需要特别注意的是命令参数指定了文本的编码方式,对于 Windows来说默认通常都是 Unicode。另外就是最后用于校验的值,这是对每一个 byte 异或运算获得的。计算方式是: buffer[0]+ buffer[1]+…+ buffer[n]。

我们还需要了解的是 Arduino 中的汉字使用的是UTF8编码 (因为 IDE 是 Unicode),例如:用下面的代码输出“宇音天下”。

char buffer[]="宇音天下";

void setup() {

  Serial1.begin(9600);

  Serial.begin(9600);

}



void loop() {



  for (int i =0;i<sizeof(buffer);i++) {

      Serial.print(buffer[i]&0xFF,HEX);

      Serial.print(' ');

    }



    Serial.println(' ');



    delay(15000);

}

 

结果如下:

E5 AE 87 E9 9F B3 E5 A4 A9 E4 B8 8B 0

其中“宇”UTF8编码:E5AE87,“音” UTF8编码:E99FB3….. 最后还有一个表示结尾的0【参考1】。

然后这个地方就让人晕掉了,为什么资料中给出来的 Unicode 是2Bytes一个汉字而上面给出来的是3个bytes?

终于我在【参考2】找到了答案,原来 Unicode规定了编码方式,但是没有规定如何存储,比如:高位在前还是在后,具体要存储多长。所以具体落地实现有 UTF8 UTF16 等等。对于我们这个情况,Arduino Java 使用的是UTF8,但是模块需要你以Unicode的编号通知它。接下来的问题就是,我有UTF8,如何转化为 Unicode?

先说Unicode转UTF8, 有下面这样的表格

Unicode符号范围     |        UTF-8编码方式

(十六进制)        |              (二进制)

----------------------+---------------------------------------------

0000 0000-0000 007F | 0xxxxxxx

0000 0080-0000 07FF | 110xxxxx 10xxxxxx

0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx

0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

 

还是以汉字严为例,演示如何实现 UTF-8 编码。

严的 Unicode 是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800 - 0000 FFFF),因此严的 UTF-8 编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx。然后,从严的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,严的 UTF-8 编码是11100100 10111000 10100101,转换成十六进制就是E4B8A5。

上面的资料依然来自【参考2】。

然后我们要尝试将E5AE87 转换为 Unicode:

首先,E5AE87 写成二进制就是下面这样
image003

根据上面的表格,切分一下(我们已经知道一个汉字是3 Bytes,所以直接使用第三行)
image004

就是说上面红色框中的对我们来说是多余的,去掉之后我们只剩下下面的数值
image005

这就是 5B87, 网页上查到的也是这样:
image006

接下来的问题就是:为什么上面给出来的0x5B87 而资料给出来的是0x8BED,对于这个问题我还真没有找到答案,0x8BED是“语”的 Unicode。只能猜测一下,要么是写手册的不小心犯错,毕竟发出来声音没人听的出来这两个字的差别;要么就是故意设计成这样。

因此,简单起见,我们可以直接计算要输出的字符,将Arduino 定义的字符串每三个bytes一组,掐头重新拼接成2个bytes。‘

//要输出的字符串

char buffer[]="宇音天下";

//实际汉字长度

#define BSIZE (sizeof(buffer)/3)

//存放转化后的汉字 Unicode值

char character[BSIZE * 2];

&nbsp;

void setup() {

Serial1.begin(9600);

Serial.begin(9600);

delay(3000);

}

&nbsp;

void loop() {

//首先输出一次Arduino 原始字符串 UTF8 的值

for (int i =0;i&lt;BSIZE*3;i++) {

Serial.print(buffer[i]&amp;0xFF,HEX);

Serial.print(' ');

}

Serial.println(' ');

&nbsp;

//将 UTF8 转化为 Unicode

for (int i =0;i&lt;BSIZE;i=i+1) {

character[i*2]=((buffer[i*3]&amp;0xF)&lt;&lt;4)+((buffer[i*3+1]&gt;&gt;2)&amp;0xF);

character[i*2+1]=((buffer[i*3+1]&amp;0x3)&lt;&lt;6)+(buffer[i*3+2]&amp;0x3F);

Serial.print(character[i*2]&amp;0xFF,HEX);

Serial.print(' ');

Serial.print(character[i*2+1]&amp;0xFF,HEX);

Serial.print(' ');

}

Serial.println(' ');

delay(15000);

}

 

实验结果就是:
image007

接下来在上面代码的基础上继续修改,集成直接串口发送

//要输出的字符串

char buffer[]="宇音天下";

//实际汉字长度

#define BSIZE (sizeof(buffer)/3)

//存放转化后的汉字 Unicode值

char character[BSIZE * 2];

&nbsp;

//根据字符串计算计算出来的送到串口的值

char output[BSIZE*2+6];

&nbsp;

void setup() {

Serial1.begin(9600);

Serial.begin(9600);

delay(5000);

}

&nbsp;

void loop() {

//首先输出一次Arduino 原始字符串 UTF8 的值

for (int i =0;i&lt;BSIZE*3;i++) {

Serial.print(buffer[i]&amp;0xFF,HEX);

Serial.print(' ');

}

Serial.println(' ');

//将 UTF8 转化为 Unicode

for (int i =0;i&lt;BSIZE;i=i+1) {

character[i*2]=((buffer[i*3]&amp;0xF)&lt;&lt;4)+((buffer[i*3+1]&gt;&gt;2)&amp;0xF);

character[i*2+1]=((buffer[i*3+1]&amp;0x3)&lt;&lt;6)+(buffer[i*3+2]&amp;0x3F);

Serial.print(character[i*2]&amp;0xFF,HEX);

Serial.print(' ');

Serial.print(character[i*2+1]&amp;0xFF,HEX);

Serial.print(' ');

}

Serial.println("");

&nbsp;

output[0]=0xFD;

output[1]=(BSIZE*2+3)&gt;&gt;8;

output[2]=((BSIZE*2+3)&amp;0xFF);

output[3]=0x01;

output[4]=0x03;

//把字符串定义搬过去

for (int i=0;i&lt;BSIZE*2;i++) {

output[i+5]=character[i];

}

//计算一个校验和

output[BSIZE*2+5]=output[0];

for (int i=1;i&lt;BSIZE*2+5;i++) {

output[BSIZE*2+5]=output[BSIZE*2+5] ^ output[i];

}

&nbsp;

for (int i =0;i&lt;BSIZE*2+6;i++) {

Serial.print(output[i]&amp;0xFF,HEX);

Serial.print(' ');

Serial1.write(output[i]);

}

&nbsp;

Serial.println(' ');

&nbsp;

delay(15000);

}

 

运行结果
image008

工作的视频可以在知乎专栏中看到 https://zhuanlan.zhihu.com/p/32856362

从上面可以看到,Arduino中是有机会将指定定义的中文字符串转换后发送出去的。但是更多时候,我们直接定义每一个字符对应的Unicode即可,虽然不是很直观,但是在编码上会省很多力气。

特别提一下:模块上面有耳机插孔,我插入了一个没有声音,并且芯片迅速发热,怀疑是兼容性上的问题。如果有朋友需要用耳机或者功放,需要特别注意一下(我怀疑是耳机插头什么地方导致短路)。

参考:

  1. http://www.qqxiuzi.cn/bianma/Unicode-UTF.php Unicode和UTF编码转换
  2. http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html

20200625 补充

用 FirBeetle (ESP32) 测试,模块使用 3.3v 供电,使用第二个串口的 TX (IO10,D6)对模块发送数据。 具体代码如下:

//要输出的字符串
char buffer[]="七点五十一分";
//实际汉字长度
#define BSIZE (sizeof(buffer)/3)
//存放转化后的汉字 Unicode值
char character[BSIZE * 2];
#include <HardwareSerial.h>

HardwareSerial Serial1(1);

//根据字符串计算计算出来的送到串口的值
char output[BSIZE*2+6];

void setup() {
  Serial1.begin(9600);
      delay(5000);
}

void loop() {
  //首先输出一次Arduino 原始字符串 UTF8 的值
  for (int i =0;i<BSIZE*3;i++) {
     Serial.print(buffer[i]&0xFF,HEX);
      Serial.print(' ');
  }
  Serial.println(' ');
  //将 UTF8 转化为 Unicode
    for (int i =0;i<BSIZE;i=i+1) {
      character[i*2]=((buffer[i*3]&0xF)<<4)+((buffer[i*3+1]>>2)&0xF);
      character[i*2+1]=((buffer[i*3+1]&0x3)<<6)+(buffer[i*3+2]&0x3F);
      Serial.print(character[i*2]&0xFF,HEX);
      Serial.print(' ');
      Serial.print(character[i*2+1]&0xFF,HEX);
      Serial.print(' ');      
    } 
  Serial.println(""); 

  output[0]=0xFD;
  output[1]=(BSIZE*2+3)>>8;
  output[2]=((BSIZE*2+3)&0xFF);
  output[3]=0x01;
  output[4]=0x03;
  //把字符串定义搬过去
  for (int i=0;i<BSIZE*2;i++) {
      output[i+5]=character[i];
    } 
  //计算一个校验和  
  output[BSIZE*2+5]=output[0];
  for (int i=1;i<BSIZE*2+5;i++) {
      output[BSIZE*2+5]=output[BSIZE*2+5] ^ output[i];
    }   
  
  for (int i =0;i<BSIZE*2+6;i++) {
      Serial.print(output[i]&0xFF,HEX);
      Serial.print(' ');
      Serial1.write(output[i]);
    }     
  
Serial.println(' ');

    delay(3000);
}

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注