android TTS TextToSpeech

前言

最近公司产品提出需求:要在一个收音机广告app上新增一个小说文本朗读的功能。我第一反应是接入讯飞或者其他平台的语音sdk,可是产品说预算有限,而那些平台需要收费,而且价格不低,让我想其他方法实现。

后面再经过baidu google之后发现android原生提供了 TextToSpeech来处理文字转语音的功能。

TextToSpeech存在的问题:

目前只支持 英文、法文、意大利文、德文、西班牙文,暂不支持中文播放

测试

我在小米手机上跑了 TextToSpeech的测试demo,发现能播报中文,查看小米手机的系统设置里发现其默认的tts是小爱同学引擎。

后来测试了华为,vivo等国产手机机型,发现都够正常播放中文文字。因为手头没有google的nexus设备,因此没有测试,但是应该是没有办法播放的。

确认详细需求

后期跟产品确定详细需求时,发现他的要求大致是希望能做一个小说朗读播放器,可以拖动播放进度,有总时长,当前播放长度,暂停、开始,播放下一章,上一章文本,以及定时关闭等功能。

开始实现

  • 先引用一个网上使用textToSpeech的原文
public class MainActivity extends AppCompatActivity  implements View.OnClickListener, TextToSpeech.OnInitListener {
    private Button speechBtn; // 按钮控制开始朗读
    private EditText speechTxt; // 需要朗读的内容
    private TextToSpeech textToSpeech; // TTS对象
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        speechBtn = (Button) findViewById(R.id.btn_read);
        speechBtn.setOnClickListener(this);
        speechTxt = (EditText) findViewById(R.id.editText);
        textToSpeech = new TextToSpeech(this, this); // 参数Context,TextToSpeech.OnInitListener
    }
    /**
     * 用来初始化TextToSpeech引擎
     * status:SUCCESS或ERROR这2个值
     * setLanguage设置语言,帮助文档里面写了有22种
     * TextToSpeech.LANG_MISSING_DATA:表示语言的数据丢失。
     * TextToSpeech.LANG_NOT_SUPPORTED:不支持
     */
    @Override
    public void onInit(int status) {
        if (status == TextToSpeech.SUCCESS) {
            int result = textToSpeech.setLanguage(Locale.CHINA);
            if (result == TextToSpeech.LANG_MISSING_DATA
                    || result == TextToSpeech.LANG_NOT_SUPPORTED) {
                Toast.makeText(this, "数据丢失或不支持", Toast.LENGTH_SHORT).show();
            }
        }
    }
    @Override
    public void onClick(View v) {
        if (textToSpeech != null && !textToSpeech.isSpeaking()) {
          // 设置音调,值越大声音越尖(女生),值越小则变成男声,1.0是常规
            textToSpeech.setPitch(0.5f);
          //设定语速 ,默认1.0正常语速
           textToSpeech.setSpeechRate(1.5f);
          //朗读,注意这里三个参数的added in API level 4   四个参数的added in API level 21
            textToSpeech.speak(speechTxt.getText().toString(), TextToSpeech.QUEUE_FLUSH, null);
        }
    }
    @Override
    protected void onStop() {
        super.onStop();
        textToSpeech.stop(); // 不管是否正在朗读TTS都被打断
        textToSpeech.shutdown(); // 关闭,释放资源
    }
}
  • 其中主要的几个方法有:
/**
 * text 需要转成语音的文字 
 * queueMode 队列方式: 
 * QUEUE_ADD:播放完之前的语音任务后才播报本次内容 
 * QUEUE_FLUSH:丢弃之前的播报任务,立即播报本次内容 
 * params 设置TTS参数,可以是null。 
 * KEY_PARAM_STREAM:音频通道,可以是:STREAM_MUSIC、STREAM_NOTIFICATION、STREAM_RING等 
 * KEY_PARAM_VOLUME:音量大小,0-1f 
 * utteranceId:当前朗读文本的id
 */
textToSpeech.speak(content, TextToSpeech.QUEUE_FLUSH, null,i+"");
// 不管是否正在朗读TTS都被打断
textToSpeech.stop();       
// 关闭,释放资源
textToSpeech.shutdown(); 
// 设置音调,值越大声音越尖(女生),值越小则变成男声,1.0是常规
textToSpeech.setPitch(0.5f);
// 设定语速,默认1.0正常语速
textToSpeech.setSpeechRate(1.5f);
  • 在我获取content文本 调用
textToSpeech.speak(content, TextToSpeech.QUEUE_FLUSH, null,i+"");

却没有正常播放声音。在对照之前可播放声音的demo后,发现除了文本外,其余内容一致。TextToSpeech的最大播放文本长度是4000字。因此我采取的策略是将一段长文本拆分成多段短文本内容,然后播报时采用

for (int i = 0; i < readContentList.size(); i++) {
    textToSpeech.speak(readContentList.get(i), TextToSpeech.QUEUE_ADD, null,i+"");
}

拆分长文本代码如下:

//长文本拆分
    public static List<String> splitContent(String content){
        //[\u4E00-\u9FA5]是unicode2的中文区间
        Pattern pattern = Pattern.compile("[^\u4E00-\u9FA5]");
        Matcher matcher = pattern.matcher(content);
        content = matcher.replaceAll("");           //提取中文文本

        int startIndex = 0;
        int contentLength = 10;
        List<String> contentList = new ArrayList<>();
        while(startIndex<content.length()-1){
            if (startIndex + contentLength > content.length()){
                contentLength = content.length()-startIndex;
            }
            String contentTemp = content.substring(startIndex,startIndex+contentLength);
            contentList.add(contentTemp);
            startIndex = startIndex + contentLength;
        }
        return contentList;
    }

我个人是将文本拆成10个字一段。

  • 总结一下:
    其实目前下来 文本朗读功能基本完成了,只需要将小说文本拆解成多段文本,然后添加到TextToSpeech中就可以了。剩下来的难点我认为主要在于播放器这一块。
    播放器的功能点有以下几点:
  1. 播放/暂停按钮
  2. 上一章/下一章文本
  3. 可拖动的进度条
  4. 定时关闭

下面开始一点一点处理,因为是公司项目,所以可能主要是记录自己开发过程中的逻辑处理思路:

首先 下面这段代码是TextToSpeech的朗读监听方法,我们可以根据 onStart(String utteranceId),和onDone(String utteranceId) 来判断 当前播放的是第几段声音(utteranceId是在调用 TextToSpeech.speak(...)时设置的最后一个参数)。

我们可以在onStart(String utteranceId)中记录当前播放的是第几段声音

textToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() {
                @Override
                public void onStart(String utteranceId) {
                    // TODO: 2019/8/15 textToSpeech 开始播放 
                    // TODO: 2019/8/15 utteranceId即为 textToSpeech.speak("","",null,i)最后一个参数i
                    
                }

                @Override
                public void onDone(String utteranceId) {
                    // TODO: 2019/8/15 当前文本播放完毕 
            
                }

                @Override
                public void onError(String utteranceId) {

                }
            });
  • 播放器 播放/暂停按钮

点击暂停时调用:

 if (textToSpeech!=null){
    textToSpeech.stop();        //退出循环播放或者说停止播报
 }

在点击播放 按钮时重新调用:

// progressIndex 为朗读监听方法onStart(String utteranceId){}中记录的当前文本进度
for (int i = progressIndex; i < readContentList.size(); i++) {
    textToSpeech.speak(readContentList.get(i), TextToSpeech.QUEUE_ADD, null,i+"");
 }

这样恢复播放会存在一个问题,例如上一段文本正朗读到第8个字,我点击暂停后再重新朗读,又会从第一个字开始朗读。
可以将每段文本拆分的更细,甚至一个字为1段来解决这个问题(我试过,但是朗读过程会有卡顿的感觉)。

  • 上/下一章播放
    获取新文本内容,清除旧文本数据后,将新文本拆分重新调用 TextToSpeech.speak()方法即可

  • 可拖动进度条
    前面已经提到,将一章小说拆分成多段文本(readContentList),那么进度条的总长度就可以根据这个多段文本的长度来设置

seekbar.setMax(readContentList.size());

其进度条时长可以通过每段朗读所需时间 * 文本长度

long seekbarTime = readTime * readContentList.size();

每段文本朗读所需时长 可根据监听方法里的两次onStart(...)做一个时间差,来计算朗读一段文本所需时长。但经过本人计算,每次朗读第一次会特别耗时,其大致每10个字的粗略值是需要耗时2800毫秒。

每次拖动进度条,根据其progress来重新定位播放位置。

  • 定时关闭
    可以重开一个子线程进行倒计时,然后执行 TextToSpeech.stop()即可
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354

推荐阅读更多精彩内容