Code For Fun, Not For Food

介绍

SAE提供了效果不错的中文分词服务,于是花了点时间做了个基于TF-IDF的关键字提取算法。

语料库来自语料库在线

TF-IDF

在把一篇文章分词过后,要从中提取出最能代表文章主题的关键字,最young最simple的办法就是基于词频(TF, term frequency):

$$TF(t, d) = f(t, d)$$

其中$t$为一个单词,$d$为文档,$f(t, d)$为这个单词在文档中出现的频率。

这样带来的问题很显然,即使通过停止词干掉一些频率极高,但对内容毫无贡献的词汇(如“的、地、得”),依然无法排除某些本身频率就很高的词汇。 比如在天朝,“中国”一词在所有文章里都有很高的出现概率,但并不见得所有文章都和天朝相关。

假如有一个语料库,包含了大量具有代表性的文章。如果一个词汇在这个语料库里大量出现,则认为这个词汇本身就是个高频词; 如果一个词汇在语料库里出现频率不高,但在一篇文章里频繁出现,显然这个词汇是这篇文章里特有的。 基于这个想法,就引入了IDF(inverse document frequency):

$$IDF(t, D) = log \frac{N}{ 1 + \|\{d \in D: t \in D\}\| }$$

TF-IDF值,就能用来判断一个词汇是否是一篇文章里的关键字:

$$ TFIDF = w(t) * TF(t, d) * IDF(t, D) $$

这里加入了一个权重函数,按词性对词汇进行过滤:

$$w(t) = \left\{ \begin{array}{l l} 0.8 & t\ is\ v.\ or\ n.\\ 0.6 & t\ is\ adv.\ or\ adj.\\ 0 & other \end{array} \right.$$

链接

上周末脑洞大开的想到能否完全用HTML5的Web Audio API实现一个吉他调音器,折腾过后这个主要的目标失败了,音频可视化的部分倒是相对完整,实现了音量、频谱图以及音高的可视化。

音高检测算法

调音器的原理其实很简单,本质上就是音高检测算法(pitch detection algorithm),实现的方式有很多[1],时域方法和频域方法都有。

乐音最基本的特征就是由一系列谐波组成,包含一个基本频率$f$,以及一系列基本频率的整数倍的子波$2f, 3f, 4f, ..., nf$,基础频率$f$的值就是乐音的音调。

所以一种简单的频域音高检测算法就是HPS(harmonic product spectrum):

$$Y(\omega) = \prod_{r = 1}^R|X(\omega r)|$$ $$\hat{Y} = \max_{\omega_i}{Y(\omega_i)}$$

其中$X$是音频在频域空间的向量表示(通常通过FFT得到),HPS就是求一个频率的$R$个整数数倍位置的信号强度的乘积,形成一个新的频谱图$Y$。

谐波的基础频率就会在$Y$中形成波峰:

这个方法的缺点很明显,高频信号由于已经没有多少谐波被采样,因此在高频并不可靠。不过对于音乐而言,这并不是问题,钢琴的最高音C8不过才4186.01Hz,而音乐文件的采样率普遍是kHz级别。

HTML5 Audio API

用Audio API做音频可视化至少会创建三个对象:AudioContext, AudioSourceAnalyzerNode。 前两者的作用很显然不用多说,AnalyzerNode提供实时的FFT数据。

首先当然是创建一个context,绑定一个analyzer:

1
2
3
4
context = new AudioContext()
analyzer = context.createAnalyser()
analyzer.smoothingTimeConstant = 0.3
analyzer.fftSize = 2048

其中fftSize指定把频域空间平均分成多少份.

接着是创建source,可以是麦克风的输入:

1
2
3
4
5
6
7
8
navigator.getUserMedia audio: true, ((stream) ->
  source = context.createMediaStreamSource stream
  mic_stream = stream
  source.connect analyzer
  analyzer.connect context.destination
  console.log "Microphone open. Sample rate: #{context.sampleRate} Hz"
  ), (err) ->
    console.error "Fail to access microphone: #{err}"

也可以是载入一个音频文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
source = context.createBufferSource()

source.connect analyzer
analyzer.connect context.destination

xhr = new XMLHttpRequest()
xhr.onload = =>
  context.decodeAudioData xhr.response, ((b) ->
    console.log "Audio loaded: #{url}, Sample rate: #{context.sampleRate}Hz"
    source.buffer = b
    source.loop = true
    source.start 0.0
    ), (err) ->
      console.error "Fail to load audio: #{url}"
xhr.open "GET", url, true
xhr.responseType = 'arraybuffer'
xhr.send()

然后需要创建一个ScriptProcessorNode,和analyzer链接,这样在每帧数据可用时会执行前者的onaudioprocess,音频处理算法一般都放在这里:

1
2
3
4
5
node = context.createScriptProcessor 2048, 1, 1
node.onaudioprocess = ->
  # TODO: process audio
node.connect context.destination
analyzer.connect node

读取FFT数据:

1
2
3
4
5
node.onaudioprocess = ->
  n = analyzer.frequencyBinCount
  arr = new Uint8Array(n)
  analyzer.getByteFrequencyData arr
  # TODO: Draw

读出来的数组里包含了从低频到高频的强度,可以直接用于绘制频谱图,求和平均就是音量,也可以作为频域处理算法的输入。

数组里元素$i$对应的频率为$f_i = \frac{Sample Rate}{FFT Size} i$。

在调音器这个应用中,最大的问题就是精度,Audio API里最大取值只能是2048,在48kHz采样率时,频率分辨率只能到23.43Hz。 这个精度是无法接受的,如$E4 = 329.628, F4 = 349.228$之间只相差了19.6Hz,完全无法区分。

可视化

在这个项目里尝试了下processing.js,虽然API很清晰,但是绘图功能不足,没有原生的gradient,blur支持,只能人肉实现,于是性能一不小心就惨淡了。

源码

建议使用Chrome打开,FireFox上性能很惨淡。

作为一个数据控,爱好各种实时数据,比如中央气象台的气象雷达数据。最近在进行的一个HTML5项目,就是利用Canvas和WebGL,把外观土鳖的官方数据,变成洋气+实用的交互式地图。由于HTML5 Canvas CORS的限制,在没有服务器配合的情况下,无法跨域载入图片,然后getImageData,因此就做了一个爬虫。

经过一番试验,发现国外的免费云服务只有GAE能够访问nmc.gov.cn,确定爬虫在GAE上安家了。大致计算了下数据量,全国167个雷达站,平均10分钟更新1帧,每帧是一幅约35KB的GIF图像,一天下来就至少800+M的数据,放GAE上不现实。同时前端也需要.json文件,描述雷达站信息以及数据帧列表,所以还需要数据库存储,而GAE坑爹的datastore quota,一天只能5w次读写,爬虫大概跑3圈就爆了。在这样一些约束下,最后脑洞大开的结合GitHub API,把数据全存到一个GitHub repository里,然后再把git tree拖回来当数据库用,解决问题。

目前在GAE上部署了两个实例,分时运行,因为bandwidth quota大概只能支撑半天,然后每天删除repository重建一次,避免超过GitHub的quota。

除了爬图片,地图叠加层的对其需要知道雷达站的坐标,以及数据的范围(即图片上的“数据范围:xxx km”),前者通过之前人肉爬到的一个包含全国雷达站坐标的xml文件解决问题。 至于数据范围没有找到官方的数据,最后写了一个简单的OCR算法直接从图片上识别。

附记:用GitHub API创建commit流程

GitHub提供了Content API,可以方便的对单个文件进行CRUD操作,每次操作产生1个commit。 如果需要一次commit多个文件(比如这个爬虫每次cron会新增数百文件),这样显然不实际。 更好的做法是通过Git Data API,模拟git创建一次commit的过程。 之前做git私有传输协议的经验立即发挥作,轻车熟路的解决,流程如下:

  1. Blob API为每个文件创建一个blob
  2. Tree API为每个子文件夹创建一个tree,并添加其中文件对应的blob
  3. Commit API读出要commit分支最新的commit,以及commit对应的tree
  4. 创建root tree,未改变的文件/文件夹需要原封不动的在tree里面保留,插入/替换新增的blob/tree
  5. 创建commit,tree指向新的root tree,parent指向分支最新commit
  6. Reference API更新分支的ref,指向新创建的commit

如果操作中断或重复操作,blob/tree都不会导致多余的数据产生(纯浪费上传带宽而已)。 因为git用SHA1 Digest作为所有git object的文件名,同内容的blob/tree不会重复。 需要注意一个例外,commit由于包含了时间戳,会重复创建。

当然最好封装好的library,会省不少事。 比如我在这个项目里用到的python library是PyGitHub,几个月前给这个项目发过pull request增加了些功能,比较熟悉用起来顺手。

链接

介绍

Web Worker是HTML5标准增加的多线程方案,直接使用web worker的API开发是一件比较麻烦的事:

  • Worker脚本必须放在单独的文件里(虽然可以用blob inline worker,但是IE10/11不支持,并且官方也没有修改的意思
  • Worker和浏览器脚本需要通过message通信
  • 不利于模块化开发,不管是AMD还是CMD模块,都不能直接通过importScripts载入运行

RequireJS支持web worker,可以在worker脚本一开头importScript('path/to/require.js'),然后就愉快的一路AMD了(当然依然要message通信)。

而我最近项目里在使用的sea.js不支持,为了解决这个问题,前几天陆续向sea.js发了几个pull request,给sea.js增加了web worker支持。

在变更被merge过后,更进一步的折腾了下,把web worker的API封装了一遍,写了seajs-worker这个插件,实现了真正无缝的开发。开发者不需要关心worker的创建、生命周期管理,也不需要通过消息通信,而是直接通过清晰的异步方法调用。

链接

使用实例

示例源码在repo的的example目录里可以找到。

使用seajs-worker开发web worker只需要3步。

第一步,配置sea.js:

1
2
3
4
5
6
7
8
9
  <!-- other sutff -->
  <script>
    seajs.config({
      base: '../dist'
    });

    seajs.use('example/main');
  </script>
  <!-- other sutff -->

第二步,实现一个具体的worker类,继承自SeaWorker:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# image-worker.coffee
define (require, exports, module) ->
  SeaWorker = require '../worker'
  # require other modules directly
  require './image-util'
  # @nodoc
  class ImageWorker extends SeaWorker
    @worker_service 'sepia', (src) ->
      src.each (p) ->
        r = 0.393 * p.r + 0.769 * p.g + 0.189 * p.b
        g = 0.349 * p.r + 0.686 * p.g + 0.168 * p.b
        b = 0.272 * p.r + 0.534 * p.g + 0.131 * p.b
        src.setRGBA p.i, r, g, b, p.a
      return src

  SeaWorker.register ImageWorker

  module.exports = ImageWorker

可以看到worker类就是一个普通CMD模块的写法,可以自由require其它模块,只要执行路径里没有调用到web worker不允许访问的API(比如DOM)就行。

第三步,创建并调用worker。

创建:

1
2
Worker = require './image-worker'
worker = new Worker()

seajs-worker提供了两种调用方案。

单worker方案:

1
2
worker.sepia img_src, (err, result) ->
  ctx.putImageData result, 0, 0

可以看到,再也不需要自己去实现message通信,只需要一个简单的方法调用。返回值的方法除了以上callback的方式以外,如果seajs-worker检测到了Q的存在,就会返回一个promise对象,此时就可以使用Promise/A+ Pattern避免callback的嵌套:

1
2
worker.sepia img_src
  .then (result) -> ctx.putImageData result, 0, 0

多worker方案

仅仅使用一个worker还不足以发挥多线程的威力,seajs-worker提供了map-reduce语法,一句话实现worker pool:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Divide image into segments
segs = [seg_1, seg_2, ..., seg_n]  

# Worker count
n = 10

# Map-Reduce
Worker.map segs, 'sepia', n
  .then (dsts) ->
    Worker.reduce dsts, ((ctx, s, i) ->
      ctx.putImageData s, 0, i * step
      return ctx
      ), dst_ctx

以上代码把图像分成若干块,放到一个Array里,然后通过map把每块交给一个worker进行处理,指定同时最多有10个worker进行操作。完成后传回一个Array,分别对应每块图像的处理结果,由reduce绘制到canvas上。

这篇blog用web worker API细线了同样的功能,对比起来代码的清晰程度显而易见。

JavaScript中使用

seajs-worker为JavaScript开发者提供了几个helper方法,例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
define(function(require, exports, module) {
  // Require base
  SeaWorker = require('path/to/sea/worker');

  // Create a derived class
  var Derived = SeaWorker.extend({
    field: 'I am a field',
    method: function () {
      // I'm a function exists in both worker and browser
    },
    constructor: function (n) {
      this.n = n;
      // Call parent constructor if needed
      // this.__super(n);
    }
  });

  // Worker side methods
  Derived.inWorker("methodInWorkerOnly", function() {});

  // Browser side methods
  Derived.inBrowser("methodInBrowserOnly", function() {});

  // Worker service, running in worker, called from browser
  Derived.service("foo", function() {});

  // Register
  SeaWorker.register(Derived);

  module.exports = Derived;
});

项目状态

目前seajs-worker的所有代码均有文档、注释,并通过了单元测试。

API语句针对CoffeeScript设计优化,用JavaScript调用暂时还不够优雅,接下来会改善这个问题。

简介

这两天开始用WebGL做一个星图App,需要处理各种星表数据。

Yale Bright Star Catalogue (BSC, 亮星星表)包含了所有视星等6.5以上的恒星(9000+枚),基本上就是人类狗眼能看到的所有恒星了。

写了这个转换脚本,可以将星表数据转换为任意格式,只需要创建对应格式的underscore template就行了。

下载

Markdown格式化后的BSC 5th Edition Readme: Bright Star Catalogue, 5th Revised Ed.

转换脚本: Generic Convertor For Bright Star Catalogue

安装

  • Node.Js
  • 在脚本目录中npm install如下包:
    • coffee-script
    • async
    • underscore
  • 下载星表数据文件,解压bsc5.datnotes文件,放到和脚本相同目录
  • 和脚本一起的有一个简单的json模板(underscore template),也可以根据需要自己创建,放到和脚本相同目录

运行

命令格式:

1
$ coffee convert template_file_name output_file_name

例:

1
$ coffee convert json bsc5.json

创建模板

参考:

介绍

2048这游戏已经被玩坏了,有人把它和Flappy Bird杂交,玩不过不能忍,于是写了个AI玩之。

游戏源码修改

首先需要对游戏进行适当的修改,便于AI获取游戏状态,并输出控制量。

修改application.js,将几个关键的对象放到windows命名空间中便于访问:

1
2
3
4
5
6
window.requestAnimationFrame(function () {
	window.input = KeyboardInputManager;
	window.actuator = HTMLActuator;
  window.score = LocalScoreManager;
  window.game = new GameManager(4, KeyboardInputManager, HTMLActuator, LocalScoreManager);
});

游戏的逻辑主要在game_manager.js中实现:

游戏中的“鸟”的css class是tile-bird,障碍物的css class是tile-block,在本文中分别简称为birdblock

  • 使用game.jump()跳跃
  • bird的状态:
变量名 说明
game.birdpos 顶端的y坐标,$[0, 1]$之间,0为顶端
game.birdspd y方向速度,向下为正
  • 在任意时刻,只有4个block分别称为a, b, cdab,cd成组,有相同的水平坐标,两组block之间一直保持2个tile的距离。每组block只有3种可能状态:全在上、全在下以及一上一下,因此block的状态由两个0~2之间的数字game.ab, game.cd确定。

游戏由一个timer驱动,每一帧计算游戏状态的变化,最后调用window.actuator.actuate()方法计算元素位置,重绘游戏。

在游戏计算出元素位置并重绘后获取状态,并由AI注入控制量是最为便捷的方式。

修改html_actuator.js:

1
2
3
4
5
6
HTMLActuator.prototype.actuate = function (grid, metadata) {
  //.. Other stuff

  // Call AI
  window.AI.play(this);
}

这样对原游戏的改动就完成了,接下来只需要实现AI类,并把类对象赋值到window.AI即可。

Q-Learning

Q-Learning是一种强化学习算法,能用于寻找Markov决策过程(MDP, Markov decision process)的最优解。 MDP问题模型由一个agent,状态$S$以及每个状态对应动作(action)集合$A$构成。Agent通过完成一个动作,从一个状态$S$跳转到另一个状态$S'$,获得一定的奖励。Agent的目标就是使奖励最大化,通过学习在每个状态下最优的动作,达到这个目的。

算法的核心是矩阵$Q$,记录状态-动作对的奖励:

$$Q: S \times A \rightarrow \mathbb{R}$$

算法初始时,$Q$取设计好的值,随后agent每执行一次动作,观察新状态以及获得的奖励,通过如下公式迭代更新:

$$Q_{t+1}(s_t, a_t) = Q_{t}(s_t, a_t) + \alpha_{t}(s_t, a_t) \times [ R_{t+1} + \gamma \max Q_{t}(s_{t+1}, a) - Q_{t}(s_t, a_t) ]$$

其中:

  • $Q_{t+1}(s_t, a_t)$: 新的$Q$值
  • $Q_{t}(s_t, a_t)$: 上一时刻$Q$值
  • $R_{t+1}$: 在$s_t$时执行$a_t$后获得的奖励
  • $\alpha \in [0, 1]$: learning rate
  • $\gamma$: 折扣率

算法设计

  • 状态:

    • $\Delta y$: bird到能安全通过当前block最高点的垂直距离
    • $\Delta x$: bird到下一个block的水平方向距离
  • 动作:

    • jump: 跳跃
    • idle: 不动作
  • 奖励:

    • 死亡:-100
    • 存活:1
  • $Q$的初始化

虽然可以简单的把$Q$全初始化为0,但这样会延长学习时间。并且在很多情况下,会导致bird一直跳跃直到跳出顶端掉不下来,这样不管是jump还是idle都会被惩罚,这样永远无法学习到正确行为。另外在底部也会有同样的问题。

实际实现时,加入了先验知识:

  • 对所有$\Delta y < y_{min}$的$s$,初始化jump的reward为-100。即在上端时禁止跳跃
  • 对所有$\Delta y > n * BirdHeight$的$s$,初始化idle的reward为-5n接近1。即离最高点的距离小于bird自己高度的时候,倾向于跳跃。注意这里的reward值较小,是因为在某些组合下(如当前block在下,下一个block在上),跳跃会挂掉,值如果过大,$Q$值无法及时对惩罚做出反馈。

  • 不定状态时的随机参数

jumpaction的reward相等时,无法通过$Q$做出决策,这个时候需要随机决定采取何种行为。

实际实现时,同样没有简单的将这个概率设为0.5,而是让不跳跃的概率远大于跳跃。道理很简单,游戏的操作方式是不平衡的,玩家只能干预下落,而不能干预上升,掉得太低跳一下就行了,跳得太高就只有等死。

效果

目前实现的版本在未学习的情况下,可以一次跳跃到700+分,学习一小时后可以到1000分,到后面出错都是遇到比较极端的组合差之毫厘,重现概率不高,所以学习速度会变慢。

玩:Q Learning Flappy 2048

代码:GitHub

介绍

今天把博客从blog.catx.me迁移到了catx.me,并关闭了原来的wordpress博客,算是完成了迁移的工作。 由于在过渡期也有若干个项目引用了博客的URL,所以迁移最后需要解决的问题是改变域名过后的重定向。 重定向包括两个方面:

  • 通过原URL的访客不会死链,会自动跳转到新URL
  • 搜索引擎能自动重新索引,不会降低页面排名

实现的原理就是在每个页面的<head>部分添加两个标签:

1
2
<meta http-equiv="refresh" content="0; url=http://new.domain.com/same/relative/url/of/old/site/">
<link rel="canonical" href="http://new.domain.com/same/relative/url/of/old/site/" />

第一个是给人看的,第二个是给机器看的。

前者会自动让浏览器跳转到新的域名,后者在搜索引擎的bot下次抓取页面的时候读取,重新索引到新的URL。

对于较大的站点,人肉在每个页面的标签很是麻烦,于是做了一个hexo的主题来实现这样一个功能(当然也有其他方式,不过主题是最简单的)。这并不是一个真正的主题,因为没有任何内容会被访问者看到。这个主题唯一的用途就是生成一个结构完全相同的站点,把每个页面重定向到一个新的域名上。

使用

以把在blog.catx.me上的站点迁移到catx.me上为例,创建repo、修改DNS、修改CNAME这些部署上的细节大同小异而又千变万化,不在此说明,过程如下:

  • 创建一个hexo博客文件夹的副本
  • 在副本文件夹中安装这个主题:
1
$ git clone https://github.com/akfish/hexo-theme-redirect.git themes/redirect
  • 修改副本站点的_config.yml文件,使用主题:
1
theme: redirect
  • 修改副本站点的_config.yml文件,添加如下行指定新域名:
1
new_domain: catx.me
  • 修改副本站点的部署配置,部署到blog.catx.me(老域名)
  • 修改原站点的部署配置,部署到catx.me(新域名)

这样就完成了迁移工作,比如访问http://blog.catx.me就会自动跳转到http://catx.me

其它

如果你的站点部署在GitHub Pages上,老域名恰好在yourname.github.io repo的CNAME绑定过,那么你其它项目的GitHub Pages的URL也需要设置跳转。

比如有个项目foo,原有的gh-pages地址就是http://blog.catx.me/foo,那么就可以在副本站点中运行:

1
$ hexo new page foo

部署后就能实现跳转。需要注意的是,hexo生成的页面路径全是小写,如果服务器是区分大小写的,就需要手动在source里修改成正确的形式。

源代码

hexo-theme-redirect

介绍

技术类博客总是不可避免的要插入各种UML图,昨天偶然发现一个有意思的Javascript库Jumly,用于渲染UML sequence diagram和robustness diagram。于是制作了一个hexo插件,便于在博客中插入。

Sequence Diagram

Robustness Diagram

安装

1
$ npm install hexo-tag-uml --save

初始化

在blog文件夹中执行:

1
$ hexo uml install

_config.yml文件中添加:

1
2
plugins:
- hexo-tag-uml

在主题的.ejs文件的合适位置插入:

1
<%- partial('jumly') %>

一般而言可以放在<head>一节里,需要注意的是Jumly依赖于jQuery,如果主题里引用了其它位置的jQuery,会导致冲突。 比如hexo的默认主题landscape就在after-footer.ejs中插入了jQuery,需要将相应行去掉,替换为上面语句。 也是因为实际主题的实现哥又不同,这个插件没能实现自动修改theme layout文件。

语法

1
2
{% uml [diagram_type] %}
{% uml %}

diagram_type可以取的值为:

  • sequence
  • robustness

如果留空,默认为sequence。

示例

1
2
3
4
5
6
7
8
9
10
11
12
{% uml %}
@found "Browser", ->
  @alt {
    "[200]": -> @message "GET href resources", "HTTP Server"
    "[301]": -> @ref "GET the moved page"
    "[404]": -> @ref "show NOT FOUND"
  }
@find(".ref").css(width:256, "padding-bottom":4)
  .find(".tag").css float:"left"
get_the_moved_page.css "background-color":"#80c080"
show_not_found.css "background-color":"#f0b0b0"
{% enduml %}

效果

Jumly的表达式规则详见:Jumly Reference Manual

在线编辑器:Try Jumly

Sequence Diagram

Robustness Diagram

背景

虚拟HID驱动用于虚拟一个或多个人机交互设备,如键盘、鼠标、摇杆等,作用都懂的。 经过搜索发现一个开源项目vmulti,实现了虚拟的多点触控、鼠标、键盘、摇杆以及数位笔,省去了自己写驱动的麻烦。

编译vmulti

  1. 安装WDK
  2. 运行以管理员权限WDK build environment
  3. 进入vmulti工程文件夹,运行
1
build -wgc
  1. 把编译生成的vmulti.sysmulti.infhidkmdf.sys文件放到同一个文件夹
  2. 把WDK中的WdfCoInstaller01009.dll, devcon.exe也放到这个文件夹

驱动签名

Windows x64系统上无法安装无签名的驱动,需要进行self sign。所有操作需要在管理员权限的WDK build environment中执行。

打开Windows测试模式

要加载self sign的内核代码,需要打开windows的测试模式:

1
bcdedit /set testsigning on

重启。

创建证书

1
2
3
makecert -r -pe -ss "CatX" -n "CN=CatX Test Certificate" catx.cer
certmgr -add catx.cer /s /r localMachine root
certmgr -add catx.cer /s /r localMachine trustedpublisher

验证证书是否正确安装,运行:

1
certmgr

签名

驱动中的如下文件需要签名:

  • *.sys文件
  • *.inf中引用的*.cat文件

如果*.cat文件不存在,需要运行inf2cat创建:

1
inf2cat /driver:%driver_folder% /os:7_x64

vmulti需要签名的文件有vmulti.sys、hidkmdf.sys和kmdfsamples.cat,运行:

1
signtool sign /v /s "CatX" /n "CatX Test Certificate" /t http://timestamp.verisign.com/scripts/timestamp.dll %file_name%

验证签名:

1
signtool verify /pa /v *.cat *.sys

安装vmulti

运行:

1
devcon install vmulti.inf djpnewton\vmulti

测试

vmulti工程中包含了一个测试程序testvmulti.exe,用于测试驱动功能:

1
2
3
testvmulti.exe /multitouch
testvmulti.exe /mouse
testvmulti.exe /digitizer