Feeds:
文章
评论

Archive for 2012年2月

In this tutorial, we will work through how to create a spoken English assessment Flex application using AISpeech API and their ASSDK (ActionScript SDK).

Before you start

Ask AI Speech Ltd (mail to api@aispeech.com) for a developer account, application ID, secret key and a release version of AISpeech API ASSDK (AISpeechLib.swc).

Note that this ASSDK supports ActionScript 3.0 only. Version for ActionScript 2.0 is also available but just ask.

Headset or microphone is necessary for the demo application.

Target

We are building a spoken English assessment application. It is a Flex application. The application allows user to read a sentence in English. It then records user’s speech, and shows score of each word, respectively. The higher the score is, the better pronunciation the user has done.

(更多…)

Read Full Post »

在上一篇心得中,简述了AISpeech API ASSDK的分层和解耦设计。本篇谈封装和透明。

谈到封装,从设计角度讲,我觉得,首先要考虑的是“暴露”什么。ActionScript的代码中,需要暴露的包括对象的方法和属性,同时加上事件。

方法

原ASSDK设计的类似网络连接组件为FmsConnector。除了一个构造函数,总共设计8个public方法,7个getter或者setter以及5个实现IEventDispatcher接口需要的public方法。8个public方法列表如下:

init()

connect()

closeConnect()

callServer()

requestSpeechCore()

removeAllNetStreamEventListener()

reset()

disposeAll()

重构后,用于RTMP连接的RTMPConnection,除了一个构造函数,总共设计3个public方法,分别是

connect()

disconnect()

rpc()

几个重构时的考量:

原ASSDK的方法命名不好看。closeConnect不如disconnect。

原ASSDK中init()方法仅仅将实例的状态设为NOT_READY;这个方法被砍掉了。

原ASSDK中callServer()的实现和requestSpeechCore()的实现几乎一致,仅仅是NetConnection::call()方法的第一个参数不一样,这两个方法被统一为rpc()方法。closeConnect()、removeAllNetStreamEventListener()、reset()都或多或少用于断开连接,合并为disconnect()方法。

disposeAll()被弃用,ASSDK用于客户端,且其实现不涉及大量实例的复制,几乎不涉及内存占用/泄露的问题,Flash Player默认的垃圾回收机制够用了。

除此以外,通用的网络连接组件的实现,例如实例状态维护、连接超时维护等通用属性和方法,由一个ConnectionBase类来实现,该类还继承于EventDispatcher,把原ASSDK中那5个IEventDispatcher的方法也给省了。

少量且命名简短的public方法,让重构后的RTMPConnection的使用者看起来就舒服。此外,原ASSDK的FmsConnector类文件640行,重构后的RTMPConnection类文件400行。如果再刨除保留的注释掉的原实现的代码,有效代码加有效注释(ASDoc注释)200行多一些。

属性

最重要的需要暴露的属性是组件的状态和组件的参数;最应该避免暴露的是组件内所依赖的其他组件。就网络连接的HTTTP实现,仅通过其基类暴露了一个状态属性和一个配置参数属性。属性暴露,就ActionScript而言,一定要使用getter或者setter来确认是否只读属性。RTMP的相关实现还没有完全完成重构,但其暴露的NetStream、NetConnection的client、NetStream的client等等,最终都要避免。

事件

不记得在哪里看到过,说ActionScript所有的处理都是异步的。ActionScript中的事件的实现,相当完备可靠,但也相当的烦人。

ActionScript的NetStatus事件类,是我觉得最老太太裹脚布的一个实现,其info属性有65种值,涵盖了NetConnection、NetStream、NetGroup和SharedObject的各种各样事件类型。其中,连接服务器失败有三种可能:Failed、Rejected和InvalidApp。

原ASSDK在自定义组件(FmsConnector)和相关组件的设计上,就事件而言,有如下三个问题:

第一,对组件内系统定义的事件无选择的封装;

第二,对组件的事件,无选择的转发;

第三,自定义事件并不是组件相关的。

第三个问题好解释:原ASSDK定义了一个通用的AudioIOEvent,该Event定义了一长串事件名常量,用于ASSDK各个组件。这个AudioIOEvent足足定义了46中不同的事件。直接的问题是:如果我想确认FmsConnector类能派发多少种不同的事件,必须要在FmsConnector函数内用查找“dispatchEvent”字符串的方法加上手工统计。新ASSDK定义了NetEvent,涵盖了10中不同的和网络连接组件相关的事件。其中两种是正常事件(CONNECTED和RESPONSED),另外8中异常事件。说实话,10种事件,我嫌多。

第一个问题是这样的。经典NetStatus事件的处理,用switch case结构。原ASSDK实现中,几乎照着NetStatus事件类文档写了NetStatus各种info的处理。这里是纯的代码“看”着不爽的问题:很多处理都是直接log输出,然后就break了,如下:

switch (event.info.code)

{

    case "NetStream.Failed":
        _logger.debug("NetStream.Failed");
    break;           
    case "NetStream.Publish.BadName":
        _logger.debug("NetStream.Publish.BadName");
    break;

    case "NetStream.Play.StreamNotFound":
        _logger.debug("NetStream.Play.StreamNotFound");
    break;

}

重构后,改成

_logger.debug(event.info.code)

switch(event.info.code)

{

    仅case那些一定要处理的事件

{

不知道要节省多少行代码……

第二个问题,就代码和逻辑污染而言,是最严重的。在第一部分中介绍过,原ASSDK实现了各个组件,录音机组件使用FmsConnector,总的库函数(RecorderLib)使用录音机组件,提供给JS的接口封装使用了总的库函数。FmsConnector可以发出的十数种事件,先发给了录音机组件;录音机组件再集成了他自己支持的事件,一起发出,发给了总的库函数;总的库函数再累积其自己的和其他依赖组件的事件,一起发出来(几乎是涵盖了AudioIOEvent中涉及的60多种事件),发给提供给JS的接口封装。因此,在原ASSDK各个类文件中,到处可见长长的对NetStatus事件的处理函数……

重构中,从两个方面精简。首先,从分层和解耦的设计上分析B是否需要接受和处理A的时间。对录音机而言,其正常的工作(部分)依赖网络连接组件,但是,录音机应该仅关注网络连接组件是否可用,而不在乎其CONNECTED事件和其他网络连接异常的事件(连上了就是可用,出现连接异常,就是不可用了。)

此外,对事件进行分类。该分类是按照应用级需求做的。就应用而言,网络连接上了和网络连接出现异常,是两个重要的事件,都需要相应的在用户界面上有所响应,这两类事件不可少。各种不同的连接异常事件,可以将其原因以信息形式反馈。这样一来,网络连接组件是可能仅设计3个自定义事件的。

透明

该透明的透明;不该透明的一定不要透明;可以不透明的,就尽量不透明。

参数和数据一定要透明。就AISpeech API的使用而言,大部分是基于Web的应用,使用JSSDK。参数和数据从通过JSSDK、传到ASSDK、传到云端接入层进而转发到语音服务部分。如果中间层层设卡,各处都增加或多或少的数据格式转换、数据信息的增减,整个系统的输入和输出将越来越不可控。AISpeech API从设计伊始,就一直强调这个问题。

如上参数和透明的设计,还使得AISpeech API的各个SDK使用上的一致性很好。在最新的SDK设计中,允许由Web应用指定一个参数,直接对语音内核进行操作,各个中间环节成为真正的透明。

不该透明的是那些被封装的数据结构、内部实现逻辑、组件内的实例依赖关系、组件内的事件等等。上述封装部分其实讨论了要暴露哪些,此外,还有一个潜逻辑是:尽量少的暴露。其他的,都封装起来了,都是不透明的。

可以不透明的,很好的一个例子,是默认参数。我们做过逐行分析示例代码中各个参数是否可以通过默认方式省略。

另外一个可以不透明的例子,是组件的初始化。在重构后的代码,提供了一个“工厂”类,该类根据应用的实际情况,来选择并初始化各个组件。

Read Full Post »

GTest是Google的单元测试框架

这个题目涉及两个问题:一个是项目组织结构的规划,一个是编译/测试/发布的流程。

本文描述的内容尚未设计自动化持续集成相关配置。

示范项目结构

本项目包含两个库的源码,库源码都放在src目录下,相应的单元测试程序放在test目录下。

GTest完整的引入到项目中,跟随项目一起编译,从而保证项目整体移植时,随时可编译。

image

单元测试代码

TFoo.cpp中仅包含使用GTest的宏函数,不包含main主函数。TFoo.cpp在编译的时候,和GTest提供的gtest_main.cc一起编译。

TFoo.cpp的内容如下(假设Foo.h中声明了一个add_foo函数,执行加法)

#include "src/Foo.h"
#include "gtest/gtest.h"

TEST(TFoo, Test)
{
    ASSERT_EQ(6, add_foo(2, 4));
}

CMakeLists.txt

项目根目录级的CMakeLists.txt内容如下

cmake_minimum_required(VERSION 2.6)

add_subdirectory(gtest-1.6.0)

add_subdirectory(LibraryA)

add_subdirectory(LibraryB)

# ==== unittests ====

enable_testing()

add_test(NAME TFoo COMMAND TFoo)

add_test(NAME TBar COMMAND TBar)

LibraryA的CMakeLists.txt内容如下

cmake_minimum_required(VERSION 2.6)

project(LibraryA)

# ==== unittest TFoo ====

set(GTEST_ROOT ${PROJECT_SOURCE_DIR}/../gtest-1.6.0)

include_directories(${PROJECT_SOURCE_DIR})

include_directories(${GTEST_ROOT}/include)

ADD_EXECUTABLE(TFoo test/*.cpp ${GTEST_ROOT}/src/gtest_main.cc)
target_link_libraries(TFoo gtest gtest_main)

LibraryB的CMakeLists.txt内容和LibraryA的类似,新加了可执行单元测试TBar。

使用流程

执行源码外的cmake

cd Example

mkdir Build

cd Build

cmake ..

make可执行单元测试

make TFoo TBar

执行单元测试

make test

当然可以单独执行每一个编译好的单元测试,例如

cd Build/LibraryA

./TFoo

可以看到GTest的彩色输出。

Read Full Post »

从2011年10月份开始,零零碎碎2个月的时间,对AISpeech API的ASSDK(ActionScript SDK)进行重构。起先我是作为ASSDK的使用者来看ASSDK的设计的,后来逐步深入源码,忍不住就上手重构了。

重构是程序员的天性,我想这个问题可以单独列个调侃性的题目来写。这次重构,不完全是程序员之间互相看不惯对方代码的结果。简单说,确实有重构的必要。另外,新版本上线也折腾了一段时间了,公司负责技术文化建设的同事一直要我写些心得。一来是想详细说明这次重构是由必要的,二来还账(这也算一种技术债务吧),就准备了如下系列的题目:

AISpeech API ASSDK 重构若干心得

(一)分层和解耦

(二)封装和透明

(三)有风格和统一风格

(四)设计模式和UML

(五)单元测试

(六)第三方库

(七)再看SDK的易用性

本文从第一部分开始。

原ASSDK中分层和解耦的问题

原ASSDK的实现中,虽然有模块的设计,但实现上,各模块之间耦合太紧。我有一个比喻:当时的ASSDK好比一团胶泥,任何功能改动或者新功能添加,甚至是修改一个bug,都好比在胶泥上掏个洞或者再黏上一块。

ASSDK的核心功能是将用户的录音数据通过网络通道发送给服务器、请求计算并且获得结果。在基于RTMP协议的实现中,由于ActionScript库函数的设计,录音机设备、录音机功能和网络连接以及服务器响应四个功能被揉在了一起。

ActionScript文档中实现相关功能的实例代码总共几十行,除了准备NetConnection、NetStream、注册各类事件处理函数、建立服务器连接等,最经典的就是如下三行:

microphone = Microphone.getMicrophone();

netStream.attachAudio(microphone);

netStream.publish(fmsAppURL);

第一行说获得了麦克风实例;第二行说通过已经建立的网络连接通道发送麦克风实例采集的用户音频数据;第三行说开始录音吧。

整个ASSDK以及其所支持的应用的最核心的功能点,就在这3行,原ASSDK在实现的时候,从如上3行示例代码出发,没有对录音设备录音机功能控制网络连接处理服务器响应(处理服务器响应的功能在示范代码的网络事件处理函数中实现)这四部分从逻辑上进行解耦。

其次,我个人对这三行经典范例用于AISpeech API ASSDK有看法:从本位上讲,ASSDK的核心是一个录音机。然而上面代码直译成自然语言,其本位是网络连接 – 3行经典代码中的两行都是面向 NetStream这个对象实现的,连“开始录音”这样明确的对Recorder对象的操作,也被NetStream实现。对于ActionScript库函数这样的设计,我没意见(我猜Adobe是希望通过库函数的设计绑定用户使用其FMS)。但正是这样的设计,让ASSDK的录音机和网络连接一直紧紧的绑在一起。

接着上面展开,ASSDK公开给使用者的,是5个录音机控制接口,即初始化/开始录音/结束录音/开始回放/结束回放。原ASSDK分不开“录音机功能控制”和“网络连接”组件,就将这两者从功能示范性代码一直带到了发布的代码。

其三,当AISpeech API功能进行扩展的时候,问题一下子复杂了。需求很简答:录音机和API之间采用其他的网络协议通信,例如HTTP。由此简单的需求展开出一系列实现上的变更:

  • NetConnection和NetStream对不适用了。服务器端基于HTTP协议做了定制,ASSDK端要从Socket级别来实现连接;
  • 使用Flash Player的新功能SampleDataEvent来获得原始音频采样数据;
  • 录音回放可以播放存储于本地临时存储空间的用户录音数据;
  • ……

原ASSDK已经“实现”了上述的功能扩展,但很不幸,是以胶泥的方式。

在本系列第二部分“封装和透明”中,对原ASSDK中录音机、网络连接组件和处理服务器响应组件紧耦合的表现还另外有分析。

ASSDK 2.0版的分层和解耦设计

AISpeech API ASSDK包括四个核心组件:录音设备录音机功能控制网络连接处理服务器响应。这四部分在实现细节上,各自有各自不同的关注点,或者说责任区。

  1. 录音设备类,负责获取系统麦克风设备、属性设置、麦克风设备状态维护以及相关的操作(例如提示用户允许使用麦克风设备);
  2. 录音机功能控制类,负责开始/技术录音、开始/结束回放以及录音机状态维护;
  3. 网络连接类,负责建立网络连接、发送/接受数据、底层连接状态维护;
  4. 处理服务器响应类,负责对服务器的响应的解析和高层请求状态维护。

尝试用自然语言分层:

  • ASSDK的使用者操作录音机;
  • ASSDK的使用者获得语音计算(识别)结果。

由此导出,网络连接类和ASSDK的使用者没有半毛钱关系。

如下的各模块间的关系,也用自然语言来描述:

image

 

  • 录音机通过麦克风设备来访问硬件资源,采集用户的录音;
  • 录音机将用户的录音数据通过网络连接发送给AISpeech API服务;
  • 服务器响应处理模块通过网络和AISpeech API服务通信;

分层的设计,应该直接体现于包的设计。新版ASSDK明确实现了recorder、device、net和core四个包。前面提到的通过SampleDataEvent功能获得原始录音数据的类,很好的放在了com.aispeech.recorder包中。

这里简单说一下模块间的几个解耦原则:

明确依赖关系

模块间的依赖关系是微妙的,把依赖关系弄反了,可能在功能进一步扩展的时候时候,才发现抽象错了。我坚持用自然语言的方式来确定依赖关系:A使用/通过B完成某种功能,则A依赖B。

一旦依赖关系明确了,则被依赖组件所暴露的接口和数据应该尽量少。

面向接口设计

面向接口设计,这个没有什么疑义。这里说明一点:面向接口设计不能保证分层和模块化合理。依赖是设计顺序不能搞错了:先分层分模块且明确依赖关系,然后面向接口设计以解耦。

数据透明

ASSDK相对SDK的使用者,在其和API之间应该是透明;网络组件相对录音机和API之间,应该是透明的。合理的分层才能实现正确的数据透明。

状态聚类

组件可以有多种状态,以及多种状态间跳转的途径(事件)。例如,网络连接组件,可能发生申请连接被拒绝/申请连接超时/连接超时/连接被服务器断开等等导致“连不上服务器”,但究其根本,网络连接组件对于其使用者来说,暴露可用和不可用两个状态,以及在两个状态间切换的事件即可。不应该也不必要由网络组件的使用者(例如录音机组件)来维护网络组件的状态、事件处理等等。

 

总结

原ASSDK的终极问题在于“没有设计”。虽然从形式看,有各种包、有各种类、有各种接口和相应实现,但究其本质,没有层次、没有梳理依赖关系,从而没有合理的解耦,形成了一团胶泥。

原ASSDK的状况,有其特别的“历史原因”,即前文总结的,所有的实现都源自于ActionScript库函数及文档提供的一个原型实例。然而,在进一步工程实现的过程中,在新需求和新功能开发中,做的多,想的少,实现的多,设计的少,积少成多以致到了积重难返的地步。

Read Full Post »