-----------------------------------------------------------
第一节 编程环境
通常我们所见到的Mud大多是LpMud。LpMuds使用Unix的指令和文件结构。如果你对Unix有所了解,那么LpMud中的一些指令和它岩坦的文件结构与普通的Unix基本一样。如果你从未使用过Unix,那么它与Dos不同的是在文件的路径用"/",而不是Dos的"\".
一个典型的LpMud的文件是这样的:
/clone/player/player.c
"/clone/player/"是路径,player.c是文件名。
在多数的LpMud中,下面这些的基本的Unix指令是可以使用的:
pwd, cd, ls, rm, mv, cp, mkdir, rmdir, more, tail, cat, ed
如果从未使用过Unix,那么下面这张表也许是有用的。
pwd: 显示当前目录
cd: 改换你当前的工作目录,和Dos的cd一样。
ls: 列出指定目录下的所有文件,如果没有指定任何目录,那就列出当前目录底下的文件。和Dos的dir一样。
rm: 删除一个文件 和Dos的rmdir一样
mv: 从命名一个文件 和Dos的move一样
cp: 复制一个文件 和Dos的copy一样
mkdir: 创建一个目录
rmdir: 删除一个目录
more: 按页显示一个文件在你的当前屏幕。
cat: 显示整个文件。和Dos的type一样。
tail: 显示一个文件的结尾几行。
ed: 允许你使用Mud自带的编辑器,编辑一个文件。
-----------------------------------------------------------
第二节 Lpc程序
2.1 Lpc程序。
Lpc的程序看起来和一般的C区别不断大,语法基本一样,但是Lpc和一般的语言有着根本的不同,Lpc程序是编写一个一个的"Object"。这有什么区别呢?一般的程序是在执行粗皮桐过程中,通常有一个明显的开始和和结束。程序从一个地方开始,然后顺序执行下去,到了结束的地方就中断了。Lpc的Object不是这样的。
所谓的不同的Mud,实际上是一些不同的Lpc的Object在一个Driver的上的各种不同的表现。也就说,Lpc的Object是运行在一个Driver上的。这些Object组成了LpMud的丰富多彩的世界。Driver几乎不参与创建你所接触到的世界,它所完成的工作只是让那些Lpc的Object活动起来。Lpc的Object可能没有明显的开始和结束的标志,它可能
永远在工作。
和一般的程序一样,Lpc“程序”也是由一个或多个文件组成。一个Lpc的Object是按如下方式被执行的:Driver把和这个Object相关的文件读入内存,然后解释执行。但是要记住的是,读入内存,并不是说,它就开始按顺序执行。
2.2 Driver和Mudlib关系
在有些游戏中,整个游戏包括Driver和游戏世界都用C写好,这样能快一些,但是游戏的扩充性很差,巫师们不可能在游戏进行中添加任何东西。LpMud则相反。Driver理论上应该和玩家所接触的世界几乎没有任何直接的联系。游戏世界应该是自己独立的,而且是“即玩即加”的。这就是为什么LpMud使用Lpc作为编程语言的原因。它允许你创建一个游戏世界,再由Driver在需要时读入解释执行。Lpc甚至比C更简单,更容易明白,但是它可以创建握斗一个可以让许多人在上面游戏的世界。
在你写完一个Lpc的文件时,它存在于主机的硬盘上。在游戏进行中,当需要整个Object时,这份文件将被调入内存,一个特殊的函数被调用来初始化这个Object的一些变量。现在你不用关心什么是变量,什么是函数以及游戏本身怎样来调用这个object,你只要记住Driver从硬盘中读入一个文件,然后放在内存中,如果没有任何
错误的话。
2.3 一个Object被装人内存。
一个Object不会也不必有一个特点的地方让Driver去执行它,通常Drvier会在Object中找一个地方去初始化它。一般都是这个函数叫做create()。
Lpc的Object是一些变量(它的值能变化)和函数(函数通常是用来 *** 纵那些变量的一段程序)的组合。函数 *** 纵变量的方式有:调用其他函数,使用Driver内部定义的函数(efun),基本的Lpc表达式以及流控制。
我们来看个变量的例子: wiz_level。这个变量记录你的巫师等级,如果是0呢,通常是普普通通的玩家了。这个值如果越大就表示你的巫师等级越高。这个也同时控制了你能不能执行一些巫师指令。基本上来说,一个Object就是一些变量“堆”在一起的东西。一个Object改变了,也就是某一个或者一些变量改变了。总的来说,一个Object如果要被内存中的另一个Object调用,Driver会去找这个Object的那堆变量放在哪里,如果这些变量没有值,那么Driver会调用一个特定的函数create来初始化这些变量。
但是create()不是Lpc代码开始执行的地方,只是大多数的Object从这里开始。事实上,create()可以不存在。如果这个Object不需要对变量初始化,那么create()可以不存在。那么这样的Object开始执行的地方就完全的不同于一般的Object,可以从任何地方开始。
那么究竟什么是Lpc的Object?Lpc的Object就是一堆变量的集合,它带有一个或者更多的函数来 *** 纵控制这些变量,函数的排列顺序是无所谓的,随便那个排在前面对这个Object的特性没有影响。
2.3 代码风格
在上面说过函数的顺序对这个Object的特性是毫无影响的。但是一个有着良好代码风格的程序对LpMud是很重要的。因为LpMud通常不会也不可能是一个人完成的,如果程序没有较好的可读性,那么别人理解你的“作品”是很困难的。而且有个良好的程序风格能给人以优雅的感觉,因此希望大家写的Lpc程序能有个好的风格。大家中的有些人可能以后会加入XO team创建自己梦想中的世界,我们要求你采用如下的格式书写程序。
2.3.1 头文件
在一个文件的开头是一段说明。采用如下格式:
/* /u/trill/obj/test.c
* from XO Object Library
* 测试用的Object
* created by trill 19970808
* version @(#) test.c 2.1@(#)
* last modified by trill 19971008
* 测试tell_wizard这个simul_efun
*/
第一行是这个文件的绝对路径,就是全路径。
第二行是它所在的Mudlib
第三行是它的功能的简单的描述,可以超过一行。
第四行是这个文件的作者和创建时间。
第五行是它的版本号,可能做了多次修改,甚至可能会重写,这个数字2.1标志了它大概做过多少次改动。
第六行是最后一次修改的人和时间。
第七行是最后一些修改什么东西。
对于一个Object我们要求必须有这样一段说明,特别是前面的五行必须存在,如果做了改动那么最后两行也要加上。这样一般的一个Object,我们从这段说明就能了解到一些很重要的信息。
下面是include一些文件和继承(inherit)一些Object。
#include <ansi.h>
#include "include/test.h"
inherit NPC
先系统的文件,后自己定义的一些头文件。特别要求的是必须有个和这个Object同名的".h"文件,比如"test.h"放在这个Object所在的目录的下一级目录"include"底下,就是说在include部分的最后一行是#include "include/test.h"。
在test.h定义所有在test.c用到的函数的原形,以及定义一些宏和常量。
这样做的好处是:
第一不用出现一个函数在引用时没有说明,
第二如果想知道这个Object有什么函数,直接看这个文件就可以了,不必去看那个test.c,可能test.c非常长。
第三如果建立一个help系统,用来查询每个Object存在的函数,那么这样直接去读test.h就可以,否则是一件很麻烦的事。
关于inherit我们在继承部分再说。
2.3.2 变量说明
在变量说明部分,大家最好在每个变量后面加一个简单的说明。
2.3.3 函数
一个Object的函数的顺序和名字对这个Object的表现是毫无影响的。但是为了让这个Object有良好的可读性,我们要求一个Object的函数按如下方式排列和命名:
首先是变量的接口部分,这些函数统一用Set+变量名来改变该变量的值,用Get+变量来返回变量的值。比如
static int level
void SetLevl(int i)
{
level = i
}
int GetLevel()
{
return level
}
其次是一些 *** 纵和控制变量的一些函数。比如
void AddLevel(int i)
{
一个Object的函数的顺序和名字对这个Object的表现是毫无影响的。但是为了让这个Object有良好的可读性,我们要求一个Object的函数按如下方式排列和命名:
首先是变量的接口部分,这些函数统一用Set+变量名来改变该变量的值,用Get+变量来返回变量的值。比如
static int level
void SetLevl(int i)
{
level = i
}
int GetLevel()
{
return level
}
其次是一些 *** 纵和控制变量的一些函数。比如
void AddLevel(int i)
{
level += i
}
这两类函数要求每个单词的第一字母大写。
再是一些Object所能做的事件(event),比如战斗,结婚等等。比如
void eventQuit()
{
...
}
这些函数要求事件的每个单词的第一字母大写,比如eventFight,eventMarry等等。
再下面的是由Driver调用的一些函数,比如create(), heart_beat,setup()。
最后是一些这个Object自己私有的函数,完成一些特别的功能。这些函数通常让要求每个单词的小写,中间用下划线(_)隔开。
要注意的是每个函数之间用一个空行隔开。
这些是对一个文件的整体要求,如果你有兴趣将来在XO team写程序,最好从现在开始就养成这样的编程习惯。如果你是别的Mud里面的巫师,我想一个Mud里面最好也有一个统一的整齐的风格。
也许你会问,这样要求有必要吗?这样太麻烦,程序写了自己能明白就可以了。这是不对的,LpMud是大家合作的项目,如果你做的程序别人没法看懂,不知道写的东西里面有些什么,能调用什么函数,那么实际上你写的东西是失败的,没人会去用它,它可能永远“死”在硬盘上。而且函数统一的命名法能尽快找到你所需要的函数,同时也能提高整个程序的可读性。
对于代码风格XO还有一些别的要求,我们将在以后的文章中介绍,如果你加入了XO team,代码风格将是第一篇要读的文章。
小结:
关于Lpc程序和编程环境,就介绍到这里。看完这一章,我想大家要记住的是LpMud是采用Lpc做为编程语言,Unix文件结构作为文件组织形式。Lpc是编写Object的一种语言,它的程序没有特殊的开始和结束的标志。如果Object被使用到,那么它被调入内存,如果这个Object有一个叫create()的函数,首先被执行,来初始化一些变量。
Lpc的Object是一堆变量的集合,同时带有一些能 *** 纵改变这些变量的函数。Lpc的代码风格,我想一个Mud最好有一个统一的风格,特别的XO有自己的特别的要求。
题外话:
当了好久的巫师,也用Lpc写了一些东西。我一直在试着理解Lpc,因为以我看如果一个巫师没有真正理解Lpc,他就不可能真正理解LpMud。理解Lpc并不仅仅意味着会使用它,许多巫师能使用它但是并不真正理解它。我希望在这个Lpc的介绍文章,能给大家一个Lpc的整体的印象,真正把握和理解Lpc,能创造自己心中梦想的世界。
-----------------------------------------------------------
-----------------------------------------------------------
第二章 Lpc的数据类型
-----------------------------------------------------------
第一节 序言
Lpc的Object是由零个或更多一些的被一个或一个以上函数 *** 纵控制的变量组成的。在代码中函数排列的顺序是不影响Object的特性,但是影响代码的可读性。当你写的那个Object被第一次调用时,Driver将你写的代码装入内存。当每一个Object被调入内存时,所有的变量是没有值的。create()这个函数被调用来初始化Object值。
create()这个函数在Object装入内存后立即被调用。在你读本文时可能对编程一无所知,你可能不知道什么是函数以及它是怎么调用的;或许你有了一些编程的经验,你可能对一个新创建的Object的函数相互调用过程是怎样开始感到迷惑。在这些困惑得到解决之前,你更有必要了解的是这些函数 *** 纵控制的到底是什么东西。所以你最好先来读读这一章:Lpc的数据类型。可以这么说,几乎90%的错误(包括丢失{}和())是由于错误的使用Lpc的数据类型。我认为真正理解这一章能帮助你更容易的编程。
-----------------------------------------------------------
第二节 让计算机理解你
2.1 计算机语言
众所周知的计算机懂得的语言实际上由“0”和“1”组成的机器码。计算机根本不懂得人类的自然语言,实际上它也不懂得我们使用的高级语言,比如BASIC,C,C++,Pascal等等。这些高级语言能让我们更容易的实现我们的想法。但是这些高级语言最终都要被翻译成“0”和“1”组成的计算机语言。
有两种方法能把高级语言翻译成计算机语言:编译和解释。编译类的在程序写完之后用一个编译器将其翻译成计算机语言。编译在程序执行之前就完成了。解释类的翻译的过程在程序执行时进行。由于解释类的语言程序是边执行边解释,所以一般都要比编译编译执行的慢。
不管是哪种语言,他们最终都要被翻译成0和1。但是变量,那些
你存在内存里面的变量,却不可能只是0和1。
所以你必须有一种你
使用的那种编程语言里面的方法来告诉计算机这些0和1应该被当做
整数还是字符,或者是字符串,或者别的什么东西。这样就必须使
用到数据类型。
2.2 数据类型
一个简单的例子:你现在有了一个变量,你把它叫做‘x’并且
赋予它一个十进制整数值65。在Lpc你可以这样的语句来做这件事:
------
x = 65
------
接着你可以做象下面这样的事:
-----
write(x + "\n")
y = x + 5
-----
第一行把65和字母"a"输出到屏幕上
第二行把70这个值赋于变量y
对计算机来说有个问题:它不知道你所说的 x = 65中的65什么意思.
你认为是65,但是计算机可能认为是:
00000000000000000000000001000001
但是,对计算机来说,字母'A'也是被当做:
00000000000000000000000001000001
所以,当你想让计算机明白 write( x + "\n" ), 它必须有一种方法
知道你想看到的是65而不是'A'.
计算机就是通过数据类型来区分65和'A'. 一种数据类型简单的说就
是在内存的某处, 那里代表了或者说指向某个给定的变量, 这些内存
储存的数据是什么类型的. 每个LPC的变量都必须有它对应的变量类型.
在上面给的例子, 本应在那些代码之前有下面一行:
-----
int x
----
这一行告诉Driver x应该指向什么类型的值, 它应该被当做数据类型'int'
来使用. 'int' 是一个32位的整数. 到这里, 你应该有数据类型的基本
印象, 以及为什么必须有数据类型. 他们可以让Driver知道计算机存在
内存里面的'0'和'1'到底是什么东西.
2.3 Lpc的数据类型
所有的LpMud的Driver都会有以下的数据类型:
void, int, string, object, mixed, int *, string *,
object *, mixed *
大多数的Driver都会有下面这些重要的数据类型:
float, mapping, float *, mapping *
有一些Driver同时还支持下面这些数据类型:
function, struct, class, char
特别的有MudOS支持的数据类型:(以v22pre8为例)
void, int, string, object, float, mapping, function,
class, mixed, int *, string *, object *, float *,
mapping *, function *, class *, mixed *
2.4 一些简单的数据类型
在Lpc入门里面将介绍以下的数据类型:
void, int, float, string, object, 以及mixed. 对于复杂的数据
类型比如: mapping, array, 以及一些不常用的类型比如: class,
function, 我将在Lpc进阶介绍. 这一节我们主要介绍三种数据类型:
int(整型), float(浮点数)和string(字符串).
一个int(整型)是一个整数, 比如1, 42, -18, 0, -10002938这些
都是整型. 在MudOS中一个整型是一个32位的整数, 有符号的整数.
在实际中int得到广泛的使用, 比如开始介绍变量中的wiz_level,
再比如生物的天赋, 年龄等都通常都是int(整型).
一个float(浮点数)是一个实数, 比如2.034, -102.3453, 0.0,
1132741034.33这些都是一个浮点数. 在MudOS中一个浮点数也是一个
32位的实数, 有符号的实数. float通常不常用.
在Object的数值性质中, 我们通常也就使用int和float, 甚至只用
int, 在变量的初始化中int和float自动被赋为0. 但是一般的Driver
比如MudOS不检查数值越界的情况, 还要注意的是这里的int和float
都是有符号的数, 这两点要注意.
string(字符串)是由一或更多的字符组成, 比如"a", "我是飞鸟!",
"42", "飞鸟15岁.", "I am Trill.", 这些都是字符串. 注意的是,
字符串都是被""括起来, 这样第一能区别象int(整型)42和string(字
符串)"42", 第二可以区别变量名(比如 room)和同名的字符串(比如
"room"). string类型的变量在初始化时, 如果没有显式的赋于一个
字符串比如: 空字符串"", 那将是0, 就是一空的指针.
作为最基本的数据类型int, float和string, 是一些复杂的数据
类型的基础. 对这些数据进行运算和 *** 作的 *** 作符, 将在后面介绍,
不过Lpc的 *** 作符和一般的C/C++的 *** 作符一致. 只是有一点, 就+
Lpc支持string和int或者float直接相加, 比如我们上面提到的:
write(x + "\n")
"\n"是一个字符, x是一个整型的变量, 在Lpc解释执行中, 自动将
x代表的数值转化成一个字符串, 然后把两个字符串接在一起.
当你在编程中使用一个变量, 你必须首先让Driver知道这个变量
代表什么样的数据类型. 这个过程叫做变量声明. 这必须在一个函
数或者一个Object的开始部分进行变量声明. 怎么做呢阿? 就是把
数据类型的名字放在你所要用的变量名前面, 举个例子:
-----
void add_x_and_y()
{
int x
int y
x = 2
y = x + x * x
}
----
上面就是一个完整的函数. 函数名就是add_x_and_y(). 在这个函数中
一开始声明落两个变量x, y都是int.
下面介绍MudOS支持的数据类型:
int
一个整数(32位).
float
一个浮点数(32位).
string
无限长的字符串.
object
指向一个对象的指针.
mapping
一个关系型数组.
function
一种特殊的指针, 指向(object, 函数名)这样一个组合.
arrays
数组的声明采用使用 '*' 跟在一种基本的类型.
void
这种类型对变量没有任何用处, 只对函数有用. 它表明这个函数
不返回任何值.
mixed
这是一种特殊的数据类型, 可以指向任何的数据类型. 如果一个
变量被声明成mixed, 那么Driver不会对它做任何检查.
class
自定义的数据类型, 类似C的struct而和C++和class不一样.
一上是MudOS支持的数据类型.
小结:
对一个变量, Driver需要知道存在计算机内存中的'0'和'1'到底
指的什么东西, 这样我们引入落数据类型. 我们学习3种简单的数据
类型, 同时了解了MudOS支持的各种数据类型. 对于各种 *** 作符, 不
同数据类型有各自不同的 *** 作符, 比如你让 "飞鸟"/"trill", 那
Driver一定会返回一个错误的. 大多数数的 *** 作符和C/C++的一样,
只是+ 还支持字符串和数字相加.
-----------------------------------------------------------
-----------------------------------------------------------
第三章 Lpc的函数
-----------------------------------------------------------
第一节 序言
在前面的介绍中,大家应该知道了Lpc的Object包含能处理变量的函数。
当函数被执行时,它的工作就是处理 *** 作变量,还有是调用(call)别的函
数。变量在函数中被改变 *** 作。变量必须有个数据类型使得计算机能明白
它指向的内存中"0"和"1"到底是什么东西。一个Object的性质通常由它的
包含的变量确定,但是它的特性的表现却是依赖于它包含的函数。一个
Object如果不含有任何一个函数那是不可想象的。那么:什么是函数。
-----------------------------------------------------------
第二节 函数
2.1 什么是函数?
和数学的函数一样,你给Lpc的函数一个值,它能返回一个值。有些语
言,比如Pascal,会区分过程和函数。Lpc和C/C++一样,没有过程,但是
明白这种区别还是有用的。Pascal叫做过程的东西,Lpc叫做类型是void
的函数。换句话说,过程就是什么都不返回的函数。Pascal叫做函数的,
必须返回一些东西。在Lpc中,最无聊的,最简单的,但也是正确的函数
是这样的:
-----
void eventDoNothing() {}
-----
这个函数不接收任何输入,不执行指令,也不返回任何值。
每一个Lpc函数都由三部分组成:
1) 函数声明
2) 函数定义
3) 函数调用
和变量一样,函数必须先有个声明。这样可以让Driver知道:
1) 这个函数将返回的是哪种数据类型。
2) 需要的输入是什么,多少。通常把输入叫做参数。
一个函数声明通常是这样的:
类型 函数名(参数1, 参数2, ..., 参数N)
下面是一个函数声明的例子,这个函数叫 DrinkWater,有一个string
类型的参数,返回的是一个int。
-----
int eventDrinkWater(string str)
-----
在上面的声明中, str是输入的参数的变量名,也可以没有。就是说可以
象下面这样声明 eventDrinkWater()
-----
int eventDrinkWater(string)
-----
函数定义就是代码,它描述了这个函数对传人的参数究竟做了些什么。
函数调用就是别的函数在任何地方使用执行了这个函数。一个函数在它
写完后永远不会被调用,那这个函数的存在的唯一意义只能是浪费内存和
硬盘。一个函数写出来的目的是为了被调用。
下面是两个函数相互调用的例子,两个函数是 eventPrintValue() 和
add(),
-----
/* 首先是函数声明,这个通常是在一个Object的开始部分。
*/
void eventPrintValue()
int add(int x, int y)
/* 其次是函数 write_vals() 的函数定义。我们假定这个函数将被调用
* 是为了描述这个Object.
*/
void eventPrintValue()
{
int x
x = add(2, 2)// 我们指定 x 接收调用函数 add() 后返回的值。
write(x + "\n")
}
/* 最后是函数 add() 的函数定义。 */
int add(int x, int y)
{
return (x + y)
}
-----
有一点是指明的,在XO的编程的风格我们要求所有的函数都必须有声
明,这个在我们最开始时候说明过。但是实际上必须有函数声明的函数
是那些被调用在函数定义之前的函数。我们规定必须有函数声明,这个
只是规定,但是它会给编程带来好处。
在这一节我们知道什么是函数,函数是由什么组成。要记住,写一个
函数的根本目的是为用它,调用它。一个函数永远不会被调用,那它就
失去了存在的价值。通常别人使用你写的函数,通常只关心它能对传人
的参数做些什么加工,就是这个函数的功能是什么,返回什么。因此一
个函数有一个好的函数名,能直接描述这个函数的功能是很重要的。我
在第一章中说明了XO规定的对函数的命名机制。采用统一的命名方式有
助于相互合作提高效率。
2.2 Efuns
也许你已经听说过efun这个词了,他们是外部定义的函数,是
externally defined function 的缩写。就是说,他们是由Mud Driver
定义好的。如果参加过Lpc的编程,或者看过Lpc的代码,你可能找到这
样的一些表达式:this_player(), strcmp(), implode(), filter(),
等等,看起来象是一个函数,而你找遍整个Object以及这个Object继承
的所有Object中都没有这些函数,这就表明他们是efun。efun存在价值
是因为他们执行起来要比一般的Object带有的函数速度快的多,为什么
快呢,因为他们是以计算机直接能理解的二进制的形式存在。对于Object
内部定义的函数,我们通常叫他们是lfun(local function)。一个巫师
主要工作也就是编写一些lfun组成的Object。
在上面的例子中的 eventPrintValue() 中调用了两个函数,第一个是
函数 add(), 这个是有你声明和定义的,这个就是lfun。第二次调用,
是调用函数 write() 这个函数通常就是efun。Driver已经替你声明和定
义好了。你所要做只是调用它。
efun被创立是为了
1) 处理一些很常用的,每天都有许多函数会调用的。
2) 处理internet socket的输入输出。
3) 以及一些Lpc很难处理的事,毕竟Lpc是C的很小的子集。
efun是用C写好的,内嵌在Driver里面的。在Mud起来之前,和Driver
一起编译好的,他们执行起来会快的多。但是正和你期望的一样,他们
的调用和你写的函数的调用方法是完全一样的。总的来说,需要关心的
和一般函数一样,它需要传入什么参数,它将会返回什么的东西。
怎样得到一些efun的信息,比如传入参数和返回的类型,通常在一个
Mud里面,你可以在类似这样的 /doc/efun 的目录底下找到,或者直接
用 help <efun名>指令就可以得到帮助。efun及其依赖于你所在的Mud
的Driver,不同的Driver带有的efun区别是很大。
对于XO,使用的是MudOS,一般的efun,只要用 help 指令就能得到
帮助,或者你多看看源码,看看别人是怎样使用的,当然你如果无论如
何也不能明白一个efun,你可以问问大巫师,他们通常会很乐意和你探
讨的。但是有一点是指出,能自己解决的问题最好自己解决。
2.3 自己动手写函数
用Lpc写Object的函数,是为了表现这个Object的特性。这个特性的
函数实际上就是一些代码按顺序排列,排列的顺序决定了这个函数。一
个函数被调用,函数的代码就按照函数定义中代码按顺序执行。在
eventPrintValue()中,下面这个语句:
-----
x = add(2, 2)
-----
必须在 efun: write() 之前调用,如果你想看到正确的结果。
为了返回这个函
凌阳单片机本身具备语音识别功能,不需要外扩语音识别模块,虽然具有一点点DSP功能,就能做到语音识别功能。如常见的语音控制机器人,语音控制小车等。我们所说的音频是指频率在20 Hz~20 kHz的声音信号,分为:波形声音、语音和音乐三种,其中波形声音就是自然界中所有的声音,是声音数字化的基础。语音也可以表示为波形声音,但波形声音表示不出语言、语音学的内涵。语音是对讲话声音的一次抽象。是语言的载体,是人类社会特有的一种信息系统,是社会交际工具的符号。音乐与语音相比更规范一些,是符号化了的声音。但音乐不能对所有的声音进行符号化。乐谱是符号化声音的符号组,表示比单个符号更复杂的声音信息内容。
将模拟的(连续的)声音波形数字元化(离散化),以便利数字计算机进行处理的过程,主要包括采样和量化两个方面。
数字音频的质量取决于:采样频率和量化位数这两个重要参数。此外,声道的数目、相应的音频设备棚族也是影响音频质量的原因
语音压缩编码中的数据量是指:数据量=(采样频率×量化位数)/8(字节数) ×声道数目。
压缩编码的目的:通过对资料的压缩,达到高效率存储和转换资料的结果,即在保证一定声音质量的条件下,以最小的资料率来表达和传送声音信息。
压缩编码的必要性:实际应用中,未经压缩编码的音频资料量很大,进行传输或存储是不现实的。 所以要通过对信号趋势的预测和冗余信息处理,进行资料的压缩,这样就可以使我们用较少的资源建立更多的信息。
举个例子,没有压缩过的CD品质的资料,一分钟的内容需要11MB的内存容量来存储。如果将原始资料进行压缩处理,在确保声音品质不失真的前提下,将数据压缩一半,5.5MB就可以完全还原效果。而在实际 *** 作中,可以依需要来选择合适的算法老和知。
常见的几种音频压缩编码:
1) 波形编码:将时间域信号直接变换为数字代码,力图使重建语音波形保持原语音信号的波形形状。波形编码的基本原理是在时间轴上对模拟语音按一定的速率抽样,然后将幅度样本分层量化,并用代码表示。译码是其反过程,将收到的数字序列经过译码和滤波恢复成模拟信号。
如:脉冲编码调制(Pulse Code Modulation,PCM)、差分脉冲编码调制(DPCM)、增量调制(DM)以及它们的各种改进型,如自适应差分脉冲编码调制(ADPCM)、自适应增量调制(ADM)、自适应传输编码(Adaptive Transfer Coding,ATC)和子带编码(SBC)等都属于波形编码技术。
波形编码特点:高话音质量、高码率,适于高保真音乐及语音。
2) 参数编码:参数编码又称为声源编码,是将信源信号在频率域或其它正交变换域提取特征参数,并将其变换成数字代码进行传输。译码为其反过程,将收到的数字序列经变换恢复特征参量,再根据特征参量重建语音信号。具体说,参数编码是通过对语音信号特征参数的提取和编码,力图使重建语音信号具有尽可能高的准确性,但重建信号的波形同原语音信号的波形可能会有相当大的差别。
如:线性预测编码(LPC)及其它各种改进型都属于参数编码。该编码比特率可压缩到2Kbit/s-4.8Kbit/s,甚至更低,但语音质量只能达到中等,特别是自然度较低。
参数编码特点:压缩比大,计算量大,音质不高,廉价!
3) 混合编码:混合编码使用参数编码技术和波形编码技术,计算机的发展为语音编码技术的研究提供了强有力的工具,大规模、超大规模集成电路的出现,则为语音编码的实现提供了基础。80年代以来,语音编码技术有了实质性的进展,产生了新一代的编码算法,这就是混合编码。它将波形编码和参数编码组合起来,克服了原有波形编码和参数编码的弱点,结合各自的长处,力图保持波形编码的高质量和参数编码的低速率。
如:多脉冲激励线性预测编码(MPLPC),规划脉冲激励线性预测编码(KPELPC),码本激励线性预测编码(CELP)等都是属于混合编码技术。其数据率和音质介于参数和波形编码之间。
总之,音频压缩技术之趋势有两个:
1)降低资料率,提高压缩比,用于廉价、低保真场合(如:电话)。
2)追求高保真度,复杂的压缩技术(如:CD)。
7.1.5 语音合成、辨识技术的介绍:
按照实现的功能来分,语音侍消合成可分两个档次:
(1) 有限词汇的计算机语音输出
(2) 基于语音合成技术的文字语音转换(TTS:Text-to-Speech)
按照人类语言功能的不同层次,语音合成可分为三个层次:
(1) 从文字到语音的合成(Text-to-Speech)
(2) 从概念到语音的合成(Concept-to-Speech)
(3) 从意向到语音的合成(Intention-to-Speech)
早期只能辨认特定的使用者即特定语者(Speaker Dependent,SD)模式,使用者可针对特定语者辨认词汇(可由使用者自行定义,如人名声控拨号),作简单快速的训练纪录使用者的声音特性来加以辨认。随着技术的成熟,进入语音适应阶段SA(speaker adaptation),使用者只要对于语音辨识核心,经过一段时间的口音训练后,即可拥有不错的辨识率。
2)非特定语者模式(Speaker Independent,SI),使用者无需训练即可使用,并进行辨认。任何人皆可随时使用此技术,不限定语者即男性、女性、小孩、老人皆可。
连续语音:
1)单字音辨认:为了确保每个字音可以正确地切割出来,必须一个字一个字分开来念,非常不自然,与我们平常说话的连续方式,还是有点不同。
2)整个句子辨识:只要按照你正常说话的速度,直接将要表达的说出来,中间并不需要停顿,这种方式是最直接最自然的,难度也最高,现阶段连续语音的辨识率及正确率,虽然效果还不错但仍需再提高。然而,中文字有太多的同音字,因此目前所有的中文语音辨识系统,几乎都是以词为依据,来判断正确的同音字。
可辨认词汇数量:
内建的词汇数据库的多寡,也直接影响其辨识能力。因此就语音辨识 的词汇数量来说亦可分为三种:
1)小词汇量(10-100)
2)中词汇量(100-1000)
3)无限词汇量(即听写机)
压缩分无损压缩和有损压缩。
无损压缩一般指:磁盘文件,压缩比低:2:1~4:1。
而有损压缩则是指:音/视频文件,压缩比可高达100:1。
凌阳音频压缩算法根据不同的压缩比分为以下几种 (具体可参见语音压缩工具一节内容):
SACM-A2000:压缩比为8:1,8:1.25,8:1.5
SACM-S480: 压缩比为80:3,80:4.5
SACM-S240: 压缩比为80:1.5
按音质排序:A2000>S480>S240
凌阳的SPCE061A是16位单片机,具有DSP功能,有很强的信息处理能力,最高时钟频率可达到49MHz,具备运算速度高的优势等等,这些都无疑为语音的播放、录放、合成及辨识提供了条件。
凌阳压缩算法中SACM_A2000、SACM_S480、SACM_S240主要是用来放音,可用于语音提示,而DVR则用来录放音。对于音乐合成MS01,该算法较繁琐,而且需要具备音乐理论、配器法及和声学知识,所以对于特别爱好者可以到我们的网站去了解相关内容,这里只给出它的API函数介绍及程序代码的范例,仅供参考。
在前面我们已经介绍过语音辨识的一些相关的内容,在这里我们给出SPCE061的特定语者辨识SD(Speaker Dependent)的一个例子以供有兴趣者参考。SD即语音样板由单个人训练,也只能识别训练某人的语音命令,而他人的命令识别率较低或几乎不能识别。
同样语音辨识也将其一些功能作成模块,并通过API调用来实现这些功能,在这里我们为大家介绍一些常用的API函数,如果有兴趣者可以登陆我们的网站去获得更多的相关内容
初始化:
【API格式】C: int BSR_DeleteSDGroup(0);
ASM:F_BSR_DeleteSDGroup(0)
【功能说明】SRAM初始化。
【参数】该参数是辨识的一个标识符,0代表选择SRAM,并初始化。
【返 回 值】当SRAM擦除成功返回0,否则,返回-1。
训练部分:
1) 【API格式】C:int BSR_Train (int CommandID, int TraindMode);
ASM:F_BSR_Train
【功能说明】训练函数。
【参数】
CommandID:命令序号,范围从0x100到0x105,并且对于每组训练语句都是唯一的。
TraindMode:训练次数,要求使用者在应用之前训练一或两遍:
BSR_TRAIN_ONCE:要求训练一次。
BSR_TRAIN_TWICE要求训练两次。
【返 回 值】训练成功,返回0;没有声音返回-1;训练需要更多的语音数据来训练,返回-2;当环境太吵时,返回-3;当数据库满,返回-4;当两次输入命令不通,返回-5;当序号超出范围,返回-6。
【备 注】
① 在调用训练程序之前,确保识别器正确的初始化。
② 训练次数是2时,则两次一定会有差异,所以一定要保证两次训练结果接近
③ 为了增强可靠性,最好训练两次,否则辨识的命令就会倾向于噪音
④ 调用函数后,等待2秒开始训练,每条命令只有1 .3秒,也就是说,当训练命令超出1.3秒时,只有前1.3秒命令有效。
辨识部分:
1)【API格式】C: void BSR_InitRecognizer(int AudioSource)
ASM:F_BSR_InitRecognizer
【功能说明】辨识器初始化。
【参数】 定义语音输入来源。通过MIC语音输入还是LINE_IN电压模拟量输入。
【返 回 值】无。
2)【API格式】C:int BSR_GetResult();
ASM:F_ BSR_GetResult
【返回值】=R1
【功能说明】辨识中获取数据。
【参数】 无。
【返 回 值】
当无命令识别出来时,返回0;
识别器停止未初始化或识别未激活返回-1;
当识别不合格时返回-2;
当识别出来时返回命令的序号。
` 【备 注】 该函数用于启动辨识,BSR_GetResult();
3)【API格式】C: void BSR_StopRecognizer(void);
ASM:F_ BSR_StopRecognizer
【功能说明】停止辨识。
【参数】无。
【返 回 值】 无。
【备 注】该函数是用于停止识别,当调用此函数时,FIQ_TMA中断将关闭。
中断部分:
【API格式】 ASM:_BSR_InitRecognizer
【功能说明】 在中断中调用,并通过中断将语音信号送DAC通道播放。
【参数】无。
【返 回 值】无。
【备 注】
① 该函数在中断FIQ_TMA中调用
② 当主程序调用BSR_InitRecognizer时,辨识器便打开8K采样率的FIQ_TMA中断并开始将采样的语音数据填入辨识器的数据队列中。
③ 应用程序需要设置一下程序段在FIQ_TMA中:
.PUBLIC _FIQ
.EXTERNAL _BSR_FIQ_Routine //定义全局变量
.TEXT
_FIQ:
PUSH R1,R4 to [SP] //寄存器入栈保护
R1 = [P_INT_Ctrl]
CALL _BSR_FIQ_Routine //调用子程序
R1 = 0x2000 //清中断标志位
[P_INT_Clear] = R1
POP R1,R4 from [SP] //寄存器组出栈
RETI
END
以下是特定人辨识的一个范例:
在程序中我们通过三条语句的训练演示特定人连续音识别,其中第一条语句为触发名称。另外两条为命令,训练完毕开始辨识当识别出触发名称后,开始发布命令,则会听到自己设置的应答,具体命令如下:
****************训练******************************************
提示音 输入语音
--------------------------------------------------------------------------------------------------------
"请输入触发名称" "警卫"
"请输入第一条命令" "开q"
"请输入第二条命令" "你在干什么?"
"请再说一遍"(以上提示音每说完一遍出现此命令)
"没有听到任何声音"(当没有检测到声音时出现此命令)
"两次输入名称不相同"(当两次输入的名称不同时出现此命令)
"两次输入命令不相同"(当两次输入的命令有差异时出现此命令)
"准备就绪,请开始辨识"(以上三条语句全部训练成功时,进入识别)
*****************识别**********************************************************************
发布命令 应答
----------------------------------------------------------------------------------------------------------
"警卫" "在"/"长官"
"开q" "q声"
"你在干什么?" "我在巡逻"/"我在休息"/"我在等人"
注意:在每次提示音结束后2-3秒再输入命令或当上次应答结束2-3秒后再发布命令
#INCLUDE "bsrsd.h"
#DEFINE NAME_ID 0x100
#DEFINE COMMAND_ONE_ID0x101
#DEFINE COMMAND_TWO_ID0x102
#DEFINE RSP_INTR 0
#DEFINE RSP_NAME 1
#DEFINE RSP_FIRE 2
#DEFINE RSP_GUARD 3
#DEFINE RSP_AGAIN 4
#DEFINE RSP_NOVOICE5
#DEFINE RSP_NAMEDIFF6
#DEFINE RSP_CMDDIFF7
#DEFINE RSP_STAR 8
#DEFINE RSP_MASTER9
#DEFINE RSP_HERE 10
#DEFINE RSP_GUNSHOT0
#DEFINE RSP_PATROL 11
#DEFINE RSP_READY 12
#DEFINE RSP_COPY 13
#DEFINE RSP_NOISY 14
//..................全程变量..................……………………………………………………………………….
int gActivated = 0
//该变量用于检测是否有触发命令,当有识别出语句为触发名称则该位置1
int gTriggerRespond[] = {RSP_MASTER, RSP_HERE, RSP_MASTER}
//第一条命令应答
int gComm2Respond[] = {RSP_PATROL, RSP_READY, RSP_COPY}
//第二条命令应答
extern void ClearWatchDog()
int PlayFlag = 0
void PlayRespond2(int Result)
//q声放音子程序
{
BSR_StopRecognizer()
SACM_A2000_Initial(1)
SACM_A2000_Play(Result, 3, 3)
while((SACM_A2000_Status()&0x0001) != 0)
{
SACM_A2000_ServiceLoop()
ClearWatchDog()
}
SACM_A2000_Stop()
BSR_InitRecognizer(BSR_MIC)
BSR_EnableCPUIndicator()
}
void PlayRespond(int Result) //放音子程序
{
BSR_StopRecognizer()
SACM_S480_Initial(1)
SACM_S480_Play(Result, 3, 3)
while((SACM_S480_Status()&0x0001) != 0)
{
SACM_S480_ServiceLoop()
ClearWatchDog()
}
SACM_S480_Stop()
BSR_InitRecognizer(BSR_MIC)
BSR_EnableCPUIndicator()//启动实时监控
}
int TrainWord(int WordID, int RespondID) //命令训练
{
int res
PlayRespond(RespondID)
while(1)
{
res = BSR_Train(WordID,BSR_TRAIN_TWICE)
if(res == 0) break
switch(res)
{
case -1: //没有检测出声音
PlayRespond(RSP_NOVOICE)
return -1
case -2: //需要重新训练一遍
PlayRespond(RSP_AGAIN)
break
case -3: //环境太吵
PlayRespond(RSP_NOISY)
return -1
case -4: //数据库满
return -1
case -5: //检测出声音不同
if(WordID == NAME_ID)
PlayRespond(RSP_NAMEDIFF) //两次输入名称不同
else
PlayRespond(RSP_CMDDIFF)//两次输入命令不同
return -1
case -6: //序号错误
return -1
}
}
return 0
}
int main()
{
int res, timeCnt=0, random_no=0
BSR_DeleteSDGroup(0) // 初始化存储器为RAM
PlayRespond(RSP_INTR) //播放开始训练的提示音
//..........训练名称..............................
while(TrainWord(NAME_ID,1) != 0)
//..........训练第一条命令.......................
while(TrainWord(COMMAND_ONE_ID,2) != 0)
//..........训练第二条命令.......................
while(TrainWord(COMMAND_TWO_ID,3) != 0)
//..........开始识别命令.........................
BSR_InitRecognizer(BSR_MIC)//辨识器初始化
BSR_EnableCPUIndicator()
PlayRespond(RSP_STAR) // 播放开始辨识的提示音
while(1)
{
random_no ++
if(random_no >= 3) random_no = 0
res = BSR_GetResult()
if(res >0)//识别出命令
{
if(gActivated)
{
timeCnt = 0
switch(res)
{
case NAME_ID:
PlayRespond(gTriggerRespond[random_no])
break
case COMMAND_ONE_ID:
PlayFlag = 1
PlayRespond2(RSP_GUNSHOT)
PlayFlag = 0
gActivated = 0
break
case COMMAND_TWO_ID:
PlayRespond(gComm2Respond[random_no])
gActivated = 0
}
}
else
{
if(res == NAME_ID)
{PlayRespond(gTriggerRespond[random_no])
gActivated = 1
timeCnt = 0
}
}
}
else if (gActivated)
{
if (++timeCnt >450) //超出定时
{PlayRespond(RSP_NOVOICE) //在设定时间内没有检测出声音
gActivated = 0
timeCnt = 0
}
}
}
}
中断程序:
.PUBLIC _FIQ
.EXTERNAL _BSR_FIQ_Routine
.EXTERNAL __gIsStopRecog //变量值 = 0 辨识器忙
// = 1 辨识器停止
.PUBLIC _BREAK,_IRQ0, _IRQ1, _IRQ2, _IRQ3, _IRQ4, _IRQ5, _IRQ6, _IRQ7
.EXTERNAL _PlayFlag
.INCLUDE s480.inc
.INCLUDE A2000.inc
.INCLUDE resource.inc
.INCLUDE hardware.inc
.TEXT
_FIQ:
push R1,R4 to [SP]
R1 = [P_INT_Ctrl]
R1 &= 0x2000
jz ?notTimerA //当不为TIQ_TMA,则转
R1 = [__gIsStopRecog]
jnz ?BSR_NotBusy
//[__gIsStopRecog]为1则转至放音处理
call _BSR_FIQ_Routine //为0,调用辨识子程序
jmp ?BSR_Busy//返回中断
?BSR_NotBusy: //放音处理
R2 = [_PlayFlag]
jnz ?Play2000 //[_PlayFlag]为1则是播放2000
call F_FIQ_Service_SACM_S480//为0,播放480
jmp ?BSR_Busy//返回中断
?Play2000: //2000播放子程序
call F_FIQ_Service_SACM_A2000
?BSR_Busy: //返回中断
R1 = 0x2000
[P_INT_Clear] = R1
pop R1,R4 from [SP]
reti
?notTimerA:
R1 = 0x8800
[P_INT_Clear] = R1
pop R1,R4 from [SP]
reti
.END
se仅仅指smart card内的集成电路,是一种狭义的小se概念,指那些具有隐樱防高阶物理攻击的IC。说LPC55没有它,也是针对无防高阶物理攻击的特性。典型升携族的例子就是我们每吵弊个都会有的yhk。欢迎分享,转载请注明来源:内存溢出
评论列表(0条)