用python写一个录音小工具

Python的paramiko,wxPython库的应用
Sound eXchange 命令行

需求

最近在给一个做语音识别的项目做QA工作。众所周知,此类人工智能方面的项目都需要一些数据收集的工作。作为一个比较小型的团队,暂时还没有使用外包团队来做大量的数据,前期数据来源都是团队内的同事录音。

我们的录音工具是树莓派+麦克风阵列,通过ssh,可以用电脑操作树莓派开始录音和停止录音。虽然从QA的角度来说,操作非常固定,但是团队同事有近一半同事不是程序猿,看到secureCRT这么恐怖的界面(此处应有图)大多还是有心理障碍。

所以为了提升用户体验(其实是没有工具就得我一个人来录了),用python做了个简单的工具,达到了点点就能用效果。顺带还整合了两三波新的需求


基本功能
  1. 可以从语料库中随机出一句话,展示在界面上给朗读人看
  2. 可以点击按钮开始录音,点击按钮结束录音。留了一个取消录音功能防止嘴飘。
  3. 有一些环境参数(性别,安静环境/嘈杂环境,距离等)可以手动填入
  4. 结束录音后可以按照格式保存为xxxx.wav文件
升级功能
  1. 支持播放已有语音的功能,用于做环境噪音合成

原理

麦克风阵列通过USB连接到树莓派,树莓派通过无线连接到插在电脑上的360随身wifi上。PC通过secureCRT或者mac的ssh语句可以远程到树莓派上执行指令。
连上以后执行下述命令即可开始录音。
$arecord -D "plughw:1,0" -r 16000 -c 10 -f S16_LE test.wav
录音指令是阻塞的,ctrl+C停止录音。

Version.1 Flask + paramiko

第一个版本就是基于上述原理的。而且因为要做个界面,所以就选了比较熟悉的Bootstrap做网页,Flask做后端,通过paramiko来执行操作。

paramiko
连接

paramiko创建ssh连接的方式如下:

import paramiko

IP = "xxx.xxx.xxx.xxx"
PORT = 22
USERNAME = "user"
PASSWORD = "pwd"
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(IP, PORT, USERNAME, PASSWORD)
执行指令

paramiko中执行指令的类有2个,一个是上述代码中的ssh,可以用stdin, stdout, stderr = ssh.exec_command(cmd)来执行cmd指令,还有一个是可以用channel来执行。

chan = ssh.get_Transport().open_session()
chan = send(cmd)
str = chan.recv(recv_buffer)

考虑到上述录音指令是阻塞的,因此如果采用ssh.exec_command(cmd)没有办法通过ctrl+C来停止录音。因此考虑采用channel.send的方法。ctrl+C可以用channel.send('\x03')来执行。
但是各种花式调用以后,一直都没能停掉录音的进程,原因不明。
因此尝试了一下直接kill掉进程可以保存数据以后,选择了简单粗暴地kill掉arecord所在进程的方法。那么接下来的问题就是获取arecord所在进程id的方法了。
执行$ps -ef | grep \"arecord\" | grep -v grep | awk '{print $2}'可以获取到arecord所在进程pid,再拼接指令$kill -9 xxxx即可杀掉进程。

前端

前端其实没啥好说,提交表单,ajax刷新页面。不过之前其实也没有写过ajax的get方法,所以记录一下

function randomclick(){
        $.get("/random", function(data){document.getElementById('scentence').innerHTML = data;})
    };
整体框架

解决了上述执行指令的问题以后,基本就是给网页提供接口了。除了随机语句的功能以外,就是

  • 开始录音:执行录音语句
  • 结束录音:结束录音进程,并将语音文件重命名为指定格式的wav文件
  • 取消录音:结束录音进程,删除临时语音文件

Version.2 wxPython+sox

后来因为有了新的需求,主要是播放wav文件和文件命名优化。回过头来看了看,觉得这种需求做成网页其实是不大合适的,毕竟根本没法处理并发的情况。所以决定用python的UI库来做成一个小工具。
之前用过python的pyqt库,但是不知道为什么安装不上pyqt(我觉得就是那阵特别不顺,没别的!),于是换成了wxPython。后来发现wxPython的media还自带音频播放功能。

wxPython
UI设计

大概描述一下需求吧

  • 文本展示框,附带2个功能按钮,只改变该文本框的内容
  • 退出时保存一些运行信息到文件中
  • 几个信息输入框,可能有select,也有可能有文本输入
  • 开始录音、结束录音和取消录音按钮
  • 选择播放录音目录的按钮和播放录音的按钮

接下来就一个一个需求点来说吧

文本展示框
sentence = wx.TextCtrl(self, style = wx.TE_MULTILINE, size = (xx, xx))
sentence.SetValue(str)
str = sentence.GetValue()

style = wx.TE_MULTILINE表示该文本框为多行文本框,会自动换行。默认为单行文本框。可以用SetValue方法设置其值,也可以用GetValue来读取文本框的值。

退出时保存信息
def __init__(self, parent, title)
  self.Bind(wx.EVT_CLOSE, self.onExit, parent)
def onExit(self, evt):
  # do sth
  evt.Skip()
  pass
  • init函数中通过self.Bind将wx.EVT_CLOSE事件绑定到self.onExit函数上。
  • 在self.onExit函数中将运行时的参数保存于文档中。
  • 注意最后需要evt.Skip()来执行关闭界面的功能
  • 特别需要注意的是,当前类需要继承wx.Frame才能使得关闭界面与当前类的wx.EVT_CLOSE事件绑定。
选择框
category_list = ["男", "女"]
classify = wx.Choice(self, -1, choices=category_list, size = (100, 48))
classify.SetSelection(0)

wx.Choice组件需要提供下拉选择列表,比如category_list,可以用SetSelection(idx)将列表中的idx项设为默认值。如果没有设定默认值,默认显示为空。

目录选择按钮
def onClickSelectDir(self, evt):
    dlg = wx.DirDialog(self, "Choose a directory:")
    if dlg.ShowModal() == wx.ID_OK:
        path = dlg.GetPath()
        self.updatelist(path)
    dlg.Destroy()

将目录选择按钮的wx.EVT_BUTTON绑定至onClickSelectDir函数上。通过path = dlg.GetPath()获取到选择的目录以后,执行自定义的updatelist(path)来完成保存数据的功能。

播放功能
def __init__(self, parent, title):
  try:
    self.mc = wx.media.MediaCtrl(self, style=wx.SIMPLE_BORDER)
  except NotImplementedError:
    self.Destroy()
    raise
  self.Bind(EVT_BUTTON, onPlay, playbutton)
def onPlay(self, evt):
  self.mc.Load(self.filename)
  time.sleep(0.5)
  self.mc.Play()
  • init中定义MediaCtrl控件
  • 把playbutton的点击事件和onPlay函数绑定
  • onPlay函数中,首先用MediaCtrl组件的Load函数加载媒体文件,sleep(0.5)后执行Play函数。
    其实这种写法并不是非常专业,之前调研的结果是需要处理EVT_MEDIA_LOADED事件,但是尝试发现将此事件与函数绑定后并未触发该事件,因此简单粗暴sleep解决。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容