Android TTS实现简单阅读器(一)

时间:2024-03-02 08:06:18

Android TTS实现简单阅读器(一)                2012-03-18 21:10:30

原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 、作者信息和本声明。否则将追究法律责任。http://vaero.blog.51cto.com/4350852/809873

Android TTS实现简单阅读器

         简单的Txt文本阅读器,主要用于介绍Google Android的TTS接口。

 
一、TTS

         在package android.speech.tts内,主要阅读下TextToSpeech.OnInitListener、TextToSpeech. OnUtteranceCompletedListener两个接口和TextToSpeech、TextToSpeech.Engine两个类。

         具体还是自己去看下SDK文档吧。(我也是完整阅读过了的^^)
 
二、TTS引擎
         以前在网上的例子,或者就我《Android基础样例》里的中文TTS例子,都是eSpeak引擎实现的。这种方式是要用其封装的TTS接口,再通过下载TTS数据使用。
         而Android的SDK中还提供了TTS服务的接口,用于供应商提供服务的。也就是语音合成服务商只管提供它的服务,开发者只管使用Android的TTS接口,用户自己安装想要的服务自己进行选择。
 

         总之呢,我用的是讯飞语音TTS v1.0。有两个文件,一个是Service程序,一个是语音数据。下载网址:http://soft.shouji.com.cn/down/22160.html

 
1)关于讯飞(貌似广告?)
         好吧,少说点了,它也提供了个开发者平台。如下:
         讯飞语音云:http://dev.voicecloud.cn/download.php?vt=1
 
         有试了下它那语音分析,话说,弹出的框框能不能好看点啊。(做个小话筒就好了么T^T)
         恩,还有,现在讯飞是要开始宣传了么?貌似3月22日什么开发者大会-_-!(又广告了?)
 
2)其他中文引擎
         参见文章:Android中文语音合成(TTS)各家引擎对比。(原网址打不开==,另外的网址就不贴了,搜下吧)
 
三、阅读器工程
         现在学乖了,直接贴些代码得了==。代码中注释应该满清晰详细了^^。
 
1)界面布局

         布局由main.xml includes header.xml & footer.xml组成,并写有了定时收起等。

 
TtsFatherActivity.java
  1. /**
  2. * 1)本类用于main.xml的布局控制。子类再去实现各控件的TTS相关功能。
  3. * 2)用继承方式实现是为了利用布局中控件的onClick属性(懒得多写代码==!)。
  4. */
  5. publicabstractclass TtsFatherActivity extends Activity {
  6. private GestureDetector gd; // 手势检测
  7. private GlobalUtil globalUtil; // 全局公用类
  8. private ScrollView scrollView; // 滚动视图
  9. private LinearLayout headerLayout, footerLayout; // 顶部、底部布局
  10. private TextView textView; // 文本标签
  11. privatestaticfinallong ANIM_DURATION = 500; // 动画时间(毫秒)
  12. privatestaticfinalint DIALOG_TEXT_LIST = 0; // 文本列表对话框id
  13. privatefinal String[] textPaths = new String[] { "one.txt", "two.txt",
  14. "浏览..." }; // assets内文本资源路径
  15. protected String textTitle; // 文本标题
  16. protected String textContent; // 文本内容
  17. private Timer timer; // 计时器
  18. privatestaticfinallong TIMEOUT = 2000; // 超时时间
  19. privatestaticfinalint TIMER_LAYOUT_OUT = 1; // 布局收起
  20. privateboolean isLayoutOut = false; // 布局收起状态
  21. /** Handler处理操作 */
  22. public Handler mHandler = new Handler() {
  23. @Override
  24. publicvoid handleMessage(Message msg) {
  25. switch (msg.what) {
  26. case TIMER_LAYOUT_OUT:
  27. /* headerLayout收起动画 */
  28. globalUtil.startTransAnim(headerLayout,
  29. GlobalUtil.AnimMode.UP_OUT, ANIM_DURATION);
  30. headerLayout.setVisibility(View.GONE);
  31. /* footerLayout收起动画 */
  32. globalUtil.startTransAnim(footerLayout,
  33. GlobalUtil.AnimMode.DOWN_OUT, ANIM_DURATION);
  34. footerLayout.setVisibility(View.GONE);
  35. isLayoutOut = true; // 重置布局收起状态
  36. break;
  37. }
  38. }
  39. };
  40. @Override
  41. publicvoid onCreate(Bundle savedInstanceState) {
  42. super.onCreate(savedInstanceState);
  43. setContentView(R.layout.main);
  44. gd = new GestureDetector(new MySimpleGesture()); // 手势检测处理
  45. globalUtil = GlobalUtil.getInstance(); // 获取全局公用类
  46. scrollView = (ScrollView) findViewById(R.id.scrollView); // 获取滚动视图
  47. headerLayout = (LinearLayout) findViewById(R.id.headerLayout); // 获取顶部布局
  48. footerLayout = (LinearLayout) findViewById(R.id.footerLayout); // 获取底部布局
  49. textView = (TextView) findViewById(R.id.textView);
  50. setText(0); // 默认显示“上邪.txt”
  51. newTimerLayoutOut(); // 定时收起布局
  52. }
  53. /** 使用GestureDetector检测手势(ScrollView内也需监听时的方式) */
  54. @Override
  55. publicboolean dispatchTouchEvent(MotionEvent ev) {
  56. gd.onTouchEvent(ev);
  57. scrollView.onTouchEvent(ev);
  58. returnsuper.dispatchTouchEvent(ev);
  59. }
  60. /** onCreateDialog */
  61. @Override
  62. protected Dialog onCreateDialog(int id) {
  63. switch (id) {
  64. case DIALOG_TEXT_LIST:
  65. returnnew AlertDialog.Builder(this).setItems(textPaths,
  66. new DialogInterface.OnClickListener() {
  67. @Override
  68. publicvoid onClick(DialogInterface dialog, int which) {
  69. if (2 == which) {
  70. // 跳转到文件浏览Activity
  71. startActivityForResult(new Intent(
  72. TtsFatherActivity.this,
  73. FileBrowserActivity.class),
  74. FileBrowserActivity.CODE_FILE_BROWSER);
  75. } else {
  76. setText(which); // 设置文本内容
  77. }
  78. }
  79. }).create();
  80. }
  81. returnsuper.onCreateDialog(id);
  82. }
  83. @Override
  84. protectedvoid onActivityResult(int requestCode, int resultCode, Intent data) {
  85. if (requestCode == FileBrowserActivity.CODE_FILE_BROWSER) {
  86. if (resultCode == RESULT_OK) {
  87. // 获得文件名称
  88. String filename = data.getExtras().getString(
  89. FileBrowserActivity.KEY_FILENAME);
  90. this.textTitle = filename;
  91. try {
  92. // FileInputStream fis = new FileInputStream(
  93. // new File(filename));
  94. // // FileInputStream不支持mark/reset操作,不该直接这样
  95. // String encoding = globalUtil.getIsEncoding(fis);
  96. // textContent = globalUtil.is2Str(fis, encoding);
  97. /**
  98. * TXT简单判断编码类型后转字符串
  99. *
  100. * ps:
  101. * 1)扯淡,3.58MB的txt读出来了==
  102. * 看来需要转成BufferedReader以readLine()方式读好些啊
  103. *
  104. * 2)TextView将大文本全部显示,这貌似...
  105. *
  106. * 时间主要花费在文本显示过程,不改进了,暂时将就吧==
  107. * 2.1)用View自定义个控件显示文本也蛮久的,未减少多少时间。
  108. * 2.2)至于AsyncTask,文本显示还是要在UI线程的==。
  109. *
  110. * 如果我们要仿个阅读器,用View自定义个控件还是必须的。
  111. * 1)分段读取大文本,可以考虑3段(前后两段用于缓冲)
  112. * 根据滑屏&显示内容等,注意文本显示衔接。
  113. * 2)滚动条可以外面套个ScrollView。由各属性判断出大文本需要显示的高度,
  114. * 重写onMeasure用setMeasuredDimension()设置好,才会有滚动条。
  115. * 当然自己用scrollTo()、scrollBy()实现动画也是好的。
  116. * 3)至于其他选中当前行啊什么的,慢慢写就成了...
  117. *
  118. * 不知道大家还有什么好的想法没?
  119. */
  120. // long time1 = System.currentTimeMillis();
  121. textContent = globalUtil.is2Str(new FileInputStream(
  122. new File(filename)));
  123. // long time2 = System.currentTimeMillis();
  124. // Log.e("TAG1", "==" + (time2 - time1) + "==");
  125. textView.setText(textContent);
  126. // long time3 = System.currentTimeMillis();
  127. // Log.e("TAG1", "==" + (time3 - time2) + "==");
  128. } catch (Exception e) {
  129. textView.setText(R.string.text_error);
  130. textContent = "";
  131. }
  132. }
  133. }
  134. }
  135. /** 设置文本内容 */
  136. privatevoid setText(int textIndex) {
  137. this.textTitle = textPaths[textIndex];
  138. try {
  139. textContent = globalUtil.is2Str(getAssets().open(textTitle),
  140. "UTF-8");
  141. textView.setText(textContent);
  142. } catch (IOException e) {
  143. textView.setText(R.string.text_error);
  144. textContent = "";
  145. }
  146. }
  147. /** 定时收起布局(已定时时重新开始定时) */
  148. protectedvoid newTimerLayoutOut() {
  149. if (null != timer) {
  150. timer.cancel();
  151. }
  152. timer = new Timer();
  153. // 超时TIMEOUT退出
  154. timer.schedule(new TimerTask() {
  155. @Override
  156. publicvoid run() {
  157. mHandler.sendEmptyMessage(TIMER_LAYOUT_OUT);
  158. }
  159. }, TIMEOUT);
  160. }
  161. /** 自定义手势类 */
  162. privateclass MySimpleGesture extends SimpleOnGestureListener {
  163. /** 双击第二下 */
  164. @Override
  165. publicboolean onDoubleTap(MotionEvent e) {
  166. if (isLayoutOut) {
  167. /* headerLayout进入动画 */
  168. headerLayout.setVisibility(View.VISIBLE);
  169. globalUtil.startTransAnim(headerLayout,
  170. GlobalUtil.AnimMode.UP_IN, ANIM_DURATION);
  171. /* footerLayout进入动画 */
  172. footerLayout.setVisibility(View.VISIBLE);
  173. globalUtil.startTransAnim(footerLayout,
  174. GlobalUtil.AnimMode.DOWN_IN, ANIM_DURATION);
  175. newTimerLayoutOut(); // 定时收起布局
  176. isLayoutOut = false; // 重置布局收起状态
  177. } else {
  178. /* headerLayout退出动画 */
  179. globalUtil.startTransAnim(headerLayout,
  180. GlobalUtil.AnimMode.UP_OUT, ANIM_DURATION);
  181. headerLayout.setVisibility(View.GONE);
  182. /* footerLayout退出动画 */
  183. globalUtil.startTransAnim(footerLayout,
  184. GlobalUtil.AnimMode.DOWN_OUT, ANIM_DURATION);
  185. footerLayout.setVisibility(View.GONE);
  186. // 取消定时收起动画
  187. if (null != timer) {
  188. timer.cancel();
  189. }
  190. isLayoutOut = true; // 重置布局收起状态
  191. }
  192. returnfalse;
  193. }
  194. /** 长按屏幕时 */
  195. @Override
  196. publicvoid onLongPress(MotionEvent e) {
  197. // 显示文本列表对话框
  198. showDialog(DIALOG_TEXT_LIST);
  199. }
  200. }
  201. }
 
2)TTS控制
         音量&语速控制也写了的^^。
 
TtsSampleActivity.java
  1. publicclass TtsSampleActivity extends TtsFatherActivity implements
  2. OnSeekBarChangeListener, TextToSpeech.OnInitListener,
  3. TextToSpeech.OnUtteranceCompletedListener {
  4. // private static final String TAG = "TtsSampleActivity"; // 日志标记
  5. private AudioManager audioManager; // 音频管理对象
  6. // TTS音量类型(AudioManager.STREAM_MUSIC = AudioManager.STREAM_TTS = 11)
  7. privatestaticfinalint STREAM_TTS = AudioManager.STREAM_MUSIC;
  8. private TextToSpeech mTts; // TTS对象
  9. privatestaticfinalint REQ_CHECK_TTS_DATA = 110; // TTS数据校验请求值
  10. privateboolean isSetting = false; // 进入设置标记
  11. privateboolean isRateChanged = false; // 速率改变标记
  12. privateboolean isStopped = false; // TTS引擎停止发声标记
  13. privatefloat mSpeechRate = 1.0f; // 朗读速率
  14. private SeekBar volumeBar, speedBar; // 音量&语速
  15. // 合成声音资源文件的路径
  16. privatestaticfinal String SAVE_DIR_PATH = "/sdcard/AndroidTTS/";
  17. privatestaticfinal String SAVE_FILE_PATH = SAVE_DIR_PATH + "sound.wav";
  18. @Override
  19. publicvoid onCreate(Bundle savedInstanceState) {
  20. super.onCreate(savedInstanceState);
  21. // 获得音频管理对象
  22. audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
  23. /* volumeBar */
  24. volumeBar = (SeekBar) findViewById(R.id.volumeBar);
  25. volumeBar.setOnSeekBarChangeListener(this);
  26. // 由当前音量设置进度(需保证进度上限=音频上限=15,否则按比例设置)
  27. volumeBar.setProgress(audioManager.getStreamVolume(STREAM_TTS));
  28. /* speedBar */
  29. speedBar = (SeekBar) findViewById(R.id.speedBar);
  30. speedBar.setOnSeekBarChangeListener(this);
  31. initDirs(SAVE_DIR_PATH); // 初始化文件夹路径
  32. }
  33. /** saveFileBtn点击事件 */
  34. publicvoid saveFile(View v) {
  35. // 将文本合成声音资源文件
  36. int resId = TextToSpeech.SUCCESS == ttsSaveFile(textContent,
  37. SAVE_FILE_PATH) ? R.string.synt_success : R.string.synt_fail;
  38. Toast.makeText(this, resId, Toast.LENGTH_SHORT).show(); // Toast提示
  39. newTimerLayoutOut(); // 重新定时收起布局
  40. }
  41. /** playFileBtn点击事件 */
  42. publicvoid playFile(View v) {
  43. ttsPlayFile(SAVE_FILE_PATH); // 播放指定的使用文件
  44. newTimerLayoutOut(); // 重新定时收起布局
  45. }
  46. /** stopBtn点击事件 */
  47. publicvoid stop(View v) {
  48. ttsStop(); // 停止当前发声
  49. newTimerLayoutOut(); // 重新定时收起布局
  50. }
  51. /** playBtn点击事件 */
  52. publicvoid play(View v) {
  53. ttsPlay(); // tts合成语音播放
  54. newTimerLayoutOut(); // 重新定时收起布局
  55. }
  56. /** settingBtn点击事件 */
  57. publicvoid setting(View v) {
  58. // 跳转到“语音输入与输出”设置界面&设置标志位
  59. isSetting = toTtsSettings();
  60. newTimerLayoutOut(); // 重新定时收起布局
  61. }
  62. /** SeekBar进度改变时 */
  63. @Override
  64. publicvoid onProgressChanged(SeekBar seekBar, int progress,
  65. boolean fromUser) {
  66. switch (seekBar.getId()) {
  67. case R.id.volumeBar:
  68. // 由设置当前TTS音量(需保证进度上限=音频上限=15,否则按比例设置)
  69. audioManager.setStreamVolume(STREAM_TTS, progress, 0);
  70. break;
  71. case R.id.speedBar:
  72. /* 需要重新绑定TTS引擎,速度在onInit()里设置 */
  73. isRateChanged = true; // 速率改变标记
  74. // 最大值为20时,以下方式计算为0.5~2倍速
  75. mSpeechRate = (progress >= 10) ? (progress / 10f)
  76. : (0.5f + progress / 20f);
  77. // 校验TTS引擎安装及资源状态,重新绑定引擎
  78. checkTtsData();
  79. break;
  80. }
  81. newTimerLayoutOut(); // 重新定时收起布局
  82. }
  83. /** SeekBar开始拖动时 */
  84. @Override
  85. publicvoid onStartTrackingTouch(SeekBar seekBar) {
  86. }
  87. /** SeekBar结束拖动时 */
  88. @Override
  89. publicvoid onStopTrackingTouch(SeekBar seekBar) {
  90. }
  91. /**
  92. * TTS引擎初始化时回调方法
  93. *
  94. * 引擎相关参数(音量、语速)等都需在这设置。
  95. * 1)创建完成后再去设置,会有意外的效果^^
  96. * 2)音量也可由AudioManager进行控制(和音乐一个媒体流类型)
  97. */
  98. @Override
  99. publicvoid onInit(int status) {
  100. if (status == TextToSpeech.SUCCESS) {
  101. mTts.setSpeechRate(mSpeechRate); // 设置朗读速率
  102. // 设置发声合成监听,注意也需要在onInit()中做才有效
  103. mTts.setOnUtteranceCompletedListener(this);
  104. if (isRateChanged) {
  105. ttsPlay(); // tts合成语音播放
  106. isRateChanged = false; // 重置标记位
  107. }
  108. }
  109. }
  110. /**
  111. * TTS引擎完成发声完成时回调方法
  112. *
  113. * 1)stop()取消时也会回调
  114. * 2)需在onInit()内设置接口
  115. * 3)utteranceId由speak()时的请求参数设定
  116. * 参数key:TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID
  117. */
  118. @Override
  119. publicvoid onUtteranceCompleted(final String utteranceId) {
  120. /* 测试该接口的Toast提示 */
  121. runOnUiThread(new Runnable() {
  122. @Override
  123. publicvoid run() {
  124. int resId = isStopped ? R.string.utte_stopped
  125. : R.string.utte_completed;
  126. // 提示文本发生完成
  127. Toast.makeText(getApplicationContext(),
  128. getString(resId, utteranceId), Toast.LENGTH_SHORT)
  129. .show();
  130. }
  131. });
  132. }
  133. /** onActivityResult */
  134. @Override
  135. protectedvoid onActivityResult(int requestCode, int resultCode, Intent data) {
  136. if (requestCode == REQ_CHECK_TTS_DATA) {
  137. switch (resultCode) {
  138. case TextToSpeech.Engine.CHECK_VOICE_DATA_PASS: // TTS引擎可用
  139. // 针对于重新绑定引擎,需要先shutdown()
  140. if (null != mTts) {
  141. ttsStop(); // 停止当前发声
  142. ttsShutDown(); // 释放资源
  143. }
  144. mTts = new TextToSpeech(this, this); // 创建TextToSpeech对象
  145. break;
  146. case TextToSpeech.Engine.CHECK_VOICE_DATA_BAD_DATA: // 数据错误
  147. case TextToSpeech.Engine.CHECK_VOICE_DATA_MISSING_DATA: // 缺失数据资源
  148. case TextToSpeech.Engine.CHECK_VOICE_DATA_MISSING_VOLUME: // 缺少数据存储量
  149. notifyReinstallDialog(); // 提示用户是否重装TTS引擎数据的对话框
  150. break;
  151. case TextToSpeech.Engine.CHECK_VOICE_DATA_FAIL: // 检查失败
  152. default:
  153. break;
  154. }
  155. }
  156. super.onActivityResult(requestCode, resultCode, data);
  157. }
  158. /** 校验TTS引擎安装及资源状态 */
  159. privateboolean checkTtsData() {
  160. try {
  161. Intent checkIntent = new Intent();
  162. checkIntent.setAction(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
  163. startActivityForResult(checkIntent, REQ_CHECK_TTS_DATA);
  164. returntrue;
  165. } catch (ActivityNotFoundException e) {
  166. returnfalse;
  167. }
  168. }
  169. /** 提示用户是否重装TTS引擎数据的对话框 */
  170. privatevoid notifyReinstallDialog() {
  171. new AlertDialog.Builder(this).setTitle("TTS引擎数据错误")
  172. .setMessage("是否尝试重装TTS引擎数据到设备上?")
  173. .setPositiveButton("是", new DialogInterface.OnClickListener() {
  174. @Override
  175. publicvoid onClick(DialogInterface dialog, int which) {
  176. // 触发引擎在TTS引擎在设备上安装资源文件
  177. Intent dataIntent = new Intent();
  178. dataIntent
  179. .setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA);
  180. startActivity(dataIntent);
  181. }
  182. }).setNegativeButton("否", null).show();
  183. }
  184. /** 跳转到“语音输入与输出”设置界面 */
  185. privateboolean toTtsSettings() {
  186. try {
  187. startActivity(new Intent("com.android.settings.TTS_SETTINGS"));
  188. returntrue;
  189. } catch (ActivityNotFoundException e) {
  190. returnfalse;
  191. }
  192. }
  193. @Override
  194. protectedvoid onStart() {
  195. checkTtsData(); // 校验TTS引擎安装及资源状态
  196. super.onStart();
  197. }
  198. @Override
  199. protectedvoid onResume() {
  200. /* 从设置返回后重新绑定TTS,避免仍用旧引擎 */
  201. if (isSetting) {
  202. checkTtsData(); // 校验TTS引擎安装及资源状态
  203. isSetting = false;
  204. }
  205. super.onResume();
  206. }
  207. @Override
  208. protectedvoid onStop() {
  209. /* HOME键 */
  210. ttsStop(); // 停止当前发声
  211. super.onStop();
  212. }
  213. @Override
  214. publicvoid onBackPressed() {
  215. /* BACK键 */
  216. ttsStop(); // 停止当前发声
  217. ttsShutDown(); // 释放资源
  218. super.onBackPressed();
  219. }
  220. /** tts合成语音播放 */
  221. privateint ttsPlay() {
  222. if (null != mTts) {
  223. isStopped = false; // 设置标记
  224. /**
  225. * 叙述text。
  226. *
  227. * 1) 参数2(int queueMode)
  228. * 1.1)QUEUE_ADD:增加模式。增加在队列尾,继续原来的说话。
  229. * 1.2)QUEUE_FLUSH:刷新模式。中断正在进行的说话,说新的内容。
  230. * 2)参数3(HashMap<String, String> params)
  231. * 2.1)请求的参数,可以为null。
  232. * 2.2)注意KEY_PARAM_UTTERANCE_ID。
  233. */
  234. HashMap<String, String> params = new HashMap<String, String>();
  235. params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, textTitle);
  236. return mTts.speak(textContent, TextToSpeech.QUEUE_FLUSH, params);
  237. }
  238. return TextToSpeech.ERROR;
  239. }
  240. // /** 判断TTS是否正在发声 */
  241. // private boolean isSpeaking() {
  242. // // 使用mTts.isSpeaking()判断时,第一次speak()返回true,多次就返回false了。
  243. // return audioManager.isMusicActive();
  244. // }
  245. /** 停止当前发声,同时放弃所有在等待队列的发声 */
  246. privateint ttsStop() {
  247. isStopped = true; // 设置标记
  248. return (null == mTts) ? TextToSpeech.ERROR : mTts.stop();
  249. }
  250. /** 释放资源(解除语音服务绑定) */
  251. privatevoid ttsShutDown() {
  252. if (null != mTts) {
  253. mTts.shutdown();
  254. }
  255. }
  256. /** 初始化文件夹路径 */
  257. privatevoid initDirs(final String dirpath) {
  258. File file = new File(dirpath);
  259. if (!file.exists()) {
  260. file.mkdirs();
  261. }
  262. }
  263. /** 将文本合成声音资源文件 */
  264. privateint ttsSaveFile(String text, final String filename) {
  265. return (null == mTts) ? TextToSpeech.ERROR : mTts.synthesizeToFile(
  266. text, null, filename);
  267. }
  268. /** 播放指定的使用文件 */
  269. privateint ttsPlayFile(final String filename) {
  270. // 如果存在FILENAME_SAVE文件的话
  271. if (new File(filename).exists()) {
  272. try {
  273. /* 使用MediaPlayer进行播放(没进行控制==) */
  274. MediaPlayer player = new MediaPlayer();
  275. player.setDataSource(filename);
  276. player.prepare();
  277. player.start();
  278. return TextToSpeech.SUCCESS;
  279. } catch (Exception e) {
  280. e.printStackTrace();
  281. return TextToSpeech.ERROR;
  282. }
  283. }
  284. return TextToSpeech.ERROR;
  285. }
  286. }