“新概念英语”、“可可英语”、“亚马逊的audible有声书”、“扇贝听力”是我目前所知道的实现英文语音和文本同步的应用。
“同步”包括两方面:javascript
被读到的单词(或句子)能够高亮显示,同步显示文本;html
选中某个单词(或句子)跳到对应的音频位置播放;html5
想要实现同步,须要知道每一个单词(或句子)在音频中的位置,称之为时间戳,相似于java
if(1.905669,2.0353742) you(2.0353742,2.1650794) really(2.1650794,2.4444444) want(2.4444444,2.643991) hear(2.643991,2.9333334) about(2.9333334,3.2226758) it(3.2226758,3.3024943)
手动去作显然是件很是费力的工做,幸运的是,已经有研究人员实现了该功能,而且开源了软件:
CMUSphinx Long Audio Aligner 项目主页python
The aligner takes audio file and corresponding text and dumps timestamps for every word in the audio. (aligner能够根据音频和相应的文本,产生音频中每一个字的时间戳)android
还有人作了进一步处理,把CMUSphinx生成的timing file格式化为一个json文件,这样:它的github主页ios
"words": [ ["if", 1.905669, 2.0353742], ["you", 2.0353742, 2.1650794], ["really", 2.1650794, 2.4444444], ["want", 2.4444444, 2.643991], ["hear", 2.643991, 2.9333334], ["about", 2.9333334, 3.2226758], ["it", 3.2226758, 3.3024943],
又有人在上二者的基础上实现了一个网页版的同步有声书:HTML5 Audio Karaoke – a JavaScript audio text aligner
点击这里能够观看它的Demogit
而我但愿作一个具备这样功能的android app,播放本身喜欢的英文小说,练习听力。github
$ git clone git@github.com:li2/TalkingBook21_AudioSync.git $ cd aligner $ python align-wav-txt.py demo/Unsigned8bitFormat.wav demo/raw.txt Running ant Updating batch Aligning text Transcription: pumas are large catlike animals which are found in americawhen reports came into london zoo...... # --------------- Summary statistics --------- Total Time Audio: 112.00s Proc: 3.28s Speed: 0.03 X real time <unk>(0.0,0.49) are(1.88,1.91) large(2.07,2.31) animals(2.34,2.94) which(2.94,3.15) are(3.15,3.29) found(3.29,3.67) in(3.67,3.76) americawhen(3.76,4.39) reports(5.22,5.92) came(5.92,6.3) into(6.33,6.66) london(6.66,7.09) zoo(7.09,7.42)...... # you can also execute: $ python align-mp3-txt.py demo/OtherFormat.mp3 demo/raw.txt
这是一个命令行工具,它最终执行的是jar -jar bin/aligner.jar your/audio/file your/txt/file
,python脚本align-wav-txt.py
在其基础上作了一层封装。shell
这里须要特别强调的是,aligner对音频文件特别挑剔,遇到过的问题之一:耗尽计算机CPU,甚至超频,最后java内存溢出。
PID COMMAND %CPU TIME #TH #WQ #PORT MEM PURG CMPRS PGRP PPID STATE BOOSTS 17203 java 718.4 64:34.94 30/8 0 95 4276M- 0B 51M 17203 15729 running *0[7] $ ps aux | grep 17203 weiyi 17203 658.3 26.2 8298480 4392764 s000 R+ 10:07上午 76:11.30 /usr/bin/java -jar bin/aligner.jar ../demo.wav ../demo.txt Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded at edu.cmu.sphinx.decoder.search.AlignerSearchManager.collectSuccessorTokens(AlignerSearchManager.java:584)
问题二:只能同步一小部分文本。
下面是我对网上下载的新概念英语3作的测试:
(章节号) 同步文本的时长/总时长 01 123/129 02 115/122 03 129/136 07 81/129 08 74/136 09 17/146 10 6/150 12 117/130 13 44/123 04,05,06,11,14,15,16,17,19,20,21,22,24,27 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException 18 54/140 23 171/178 25 0~100缺失 26 144/190
这两个问题应该都和音频格式有关。音频编辑软件audacity能够导出音频为不一样的格式,通过对比测试时发现,在导出对话框的format选项中,选择“其它非压缩音频文件:文件头WAV(Microsoft), 编码Unsigned 8 bit PCM”,这种音频格式可以获得最佳的结果。
因此我在demo文件夹里上传了两种格式的音频文件,用以对比。
audio text alignment, audio text sync
Python 2.7, java, ant, sox,
若是你在执行脚本的过程当中,遇到错误,须要根据错误提示搜索缘由并安装相关包,好比ubuntu12.04环境下执行align-mp3-txt.py时,提示错误:
no handler for file extension `mp3'
须要安装libsox-fmt-mp3
细心的你可能已经发现aligner生成的timing file其实是根据音频文件转译的文本,与原始文本相比:无标点符号(包括段落分隔符);错字(youre, dont, isnt之类缺失'
);漏字,等等。
因而我写了一个python脚本parse_timing_json.py
,它比较原文和aligner输出的json文件,把原文未被识别的文本插入到json文件中,获得一个包含完整文本和时间的json文件.
可是,这个脚本容错性很是很差,或者说,对输入文件很是挑剔,输入json文件漏字不能太多,和txt文件必须几乎一致,这个脚本才能够把时间戳和原文作匹配。 幸运的是,long-audio-align产生的json文件能够知足。
若是json文件中的字符串包含换行符,则以换行符为界拆分字符串,换行符单独拿出来。目的是方便app处理换行:app读取到换行符后,使其占据整个linearlayout,使app所显示的文本段落结构更加清晰。
另外,若是字符串包含的字符个数超过5个,则拆分这个字符串。
# 若是你能正确执行align-wav-txt.py,那么你会在demo文件夹中获得一个名为raw.json的文件(必定要使用align-wav-txt.py参生的完整的json文件): $ cd parse_timing_json/ $ python parse_timing_json.py ../aligner/demo/raw.json ../aligner/demo/raw.txt # 生成文件raw.json.out.json, $ python parse_new_line.py ../aligner/demo/raw.json.out.json # 生成文件raw.json.out.json.out.json
对比处理先后的json文件:
处理前: ["are", 1.88, 1.91], ["large", 2.07, 2.31], ["animals", 2.34, 2.94], ["which", 2.94, 3.15], ["are", 3.15, 3.29], ["found", 3.29, 3.67], ["in", 3.67, 3.76], ["americawhen", 3.76, 4.39], ["reports", 5.22, 5.92], 处理后: ["are", 2.07], ["large", 2.07], [", cat- like animals", 2.34], ["which", 2.94], ["are", 3.15], ["found", 3.29], ["in", 3.67], ["America. When reports", 5.22],
在获得了比较完整的timing file以后,剩下的工做是,如何呈现它。
初步构想是按页呈现文本,支持自动翻页和手动翻页,经过ViewPager和Fragment实现。因此问题是:
给定一个文本,如何拆分红页(每页铺满屏幕)?
好比能够拆分为3页,第1页包含21个单词,第2页包含22个单词,第3页包含23个单词。若是获得这些数据,那么就很是容易构建界面了。
public class ChapterPageAdapter extends FragmentPagerAdapter { // ViewPager的adapter的构造器,以文本文件的Uri做为参数。 public ChapterPageAdapter(Context context, FragmentManager fm, Uri jsonUri) { super(fm); mAppContext = context; assert context != null; mJsonUri = jsonUri; mPageBeginningWordIndexList = new ArrayList<Integer>(); mPageBeginningWordTimmingList = new ArrayList<Integer>(); splitChapterToPages(mJsonUri); } // 在拿到文本后,经过下面两个private函数拆分文本, // 而拆分的关键是,根据屏幕的宽度、高度,每一个单词的宽度,计算一个屏幕能够显示的单词个数,用到的一些计算宽高度的方法被抽象成一个独立的类`ChapterPageUtil.class`, // 最终获得每页首单词在文本中的序号。 private void splitChapterToPages(Uri jsonUri) {} private int totalWordsCanDisplayOnOnePage(List<String> words, int startIndex) {} @Override public int getCount() { // ViewPager托管的Fragment个数。 return 首单词序号的个数; } @Override public Fragment getItem(int poisition) { ...... // 这就是ViewPager第position页须要显示的内容。 ChapterPageFragment fragment = ChapterPageFragment.newInstance(uri, fromIndex, count); return fragment; }
public class ChapterPageFragment extends Fragment implements OnClickListener { // Create fragment instance // 每页对应一个Fragment,须要告知它要显示的文本,从哪一个单词开始显示,总共显示几个单词。 public static ChapterPageFragment newInstance(Uri jsonUri, int fromIndex, int count) { Bundle args = new Bundle(); args.putString(EXTRA_TIMING_JSON_URI, jsonUri.toString()); args.putInt(EXTRA_FROM_INDEX, fromIndex); args.putInt(EXTRA_COUNT, count); ChapterPageFragment fragment = new ChapterPageFragment(); fragment.setArguments(args); return fragment; } // Fragment是LinearLayout布局, // 每一个单词对应一个TextView,添加到 sub LinearLayout,填满后再添加到下一个 sub LinearLayout(对应下一行); // 每一个换行符独占一个 sub LinearLayout。 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {}
考虑到ViewPager Adapter和全部的Fragment都要用到文本信息,咱们把文本实例化为singleton,便于文本数据取用。
public class TalkingBookChapter { // Singletons and centralized data storage private static TalkingBookChapter sChapter; // Setting up the singleton public static TalkingBookChapter get(Context context, Uri timingJsonUri) { if (sChapter == null) { sChapter = new TalkingBookChapter(context, timingJsonUri); } return sChapter; } private TalkingBookChapter(Context context, Uri timingJsonUri) {} // 下面两个函数就是为了Fragment取它所须要显示的文本。 public List<String> getWordList(int fromIndex, int count) {} public List<Integer> getTimingList(int fromIndex, int count) {} public int size() {}
Fragment定义一个interface,在TextView被点击时调用,Activity实现它完成音频位置的调节。
翻页的时候,就更简单了,只须要override ViewPager的OnPageChangeListener.
public class ChapterPageFragment extends Fragment implements OnClickListener { private OnWordClickListener mOnWordClickListener; public void setOnWordClickListener(OnWordClickListener l) { mOnWordClickListener = l; } public interface OnWordClickListener { public void onWordClick(int msec); } @Override public void onClick(View v) { if (v instanceof TextView) { int msec = (int)v.getTag(); if (mOnWordClickListener != null) { mOnWordClickListener.onWordClick(msec); } } } } public class FullScreenPlayerActivity extends FragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { mChapterPageAdapter = new ChapterPageAdapter(this, getSupportFragmentManager(), mTimingJsonUri); mChapterPageAdapter.setOnChapterPageWordClickListener(mOnPageAdapterWordClickListener); mChapterViewPager.setAdapter(mChapterPageAdapter); // 这个Activity管理ViewPager,Fragment定义的interface被ViewPager的Adapter又包了一层, // 因此当Fragment的TextView被点击后,最终会调用到这里: private OnChapterPageWordClickListener mOnPageAdapterWordClickListener = new OnChapterPageWordClickListener() { @Override public void onChapterPageWordClick(int msec) { seekToPosition(msec); // 这个函数调用到 MediaPlayer.seekTo(int msec),调节到指定的音频位置。 } }; private ViewPager.OnPageChangeListener mOnPageChangeListener = new OnPageChangeListener() { @Override public void onPageSelected(int position) { ...... seekToPosition(mChapterPageAdapter.getPageTiming(position)); // 翻页时,调节到指定的音频位置。 }
public class FullScreenPlayerActivity extends FragmentActivity { // 这是一个更新界面的定时任务, private void updateProgress() { runOnUiThread(new Runnable() { @Override public void run() { int msec = mPlayerController.getCurrentPosition(); mChapterPageAdapter.seekChapterToTime(msec); // check if seeking time is out of selected page, if true, then set ViewPager to current item. // 若是已经读到下一页,那么执行ViewPager.setCurrentItem() ...... } }); } public class ChapterPageAdapter extends FragmentPagerAdapter { // This method will be called to notify fragment to update view in order to highlight the reading word. public void seekChapterToTime(int msec) { mSeekingTime = msec; notifyDataSetChanged(); // 调用这个方法后,getItemPosition()会被调用,咱们在getItemPosition()中完成Fragment界面的更新。 } @Override public int getItemPosition(Object object) { if (object instanceof ChapterPageFragment) { // This method called to update view in order to highlight the reading word. ((ChapterPageFragment) object).seekChapterToTime(mSeekingTime); } return super.getItemPosition(object); }
android-UniversalMusicPlayer
这是一个开源的android音乐播放器,它的项目主页
个人播放器代码不少直接拿了它的一个文件 FullScreenPlayerActivity.java 点击查看
ViewPagerIndicator
这是一个开源的Android UI,用以标示ViewPager的页,就是常见的几个小圆点。
audiosync
这个就是音频文本同步的开源命令行工具。它的项目主页。
可是呢,它包含上百兆的音频文件,在时好时坏的国外网站访问现实下,有时只有几kb的下载速度,我就folk它而后删掉它的音频文件。这样子。
2015年08月21日完成了1.0版本,App名字叫TalkingBook21(由于我叫li21嘛),你能够在这里下载App,
呐,它是这个样子的:
因为音频文件很大,因此只在app里包了一个音频,权当是个demo。
更多的音频须要在这里下载,目前仅实现了《麦田的守望者 The Catcher in the Rye》的同步,音频总时长7个小时,293M。
因此坦白的讲,这个app实质上是麦田的守望者音文同步有声读物Android App.
你须要把它解压后放入手机的外置SD卡:YourExtSDCard/TalkingBook21/,而后从新启动App(完全杀掉!),重启后的App会向你展现章节列表,点击便可播放。
这里是app的源码
这里是制做timing json file的开源命令行工具
版权声明:《一个Android音频文本同步的英文有声读物App的开发过程》由 WeiYi.Li 在 2015年08月16日写做。著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。
文章连接:http://li2.me/2015/08/my-app-android-tal...