录音机





0/5 (0投票)
将您的Android变成一个记事设备
斯科蒂:“电脑……电脑……”
(麦考伊把鼠标递给斯科蒂)
斯科蒂:“是。你好,电脑。”
尼科尔斯:“直接用键盘就行。”
斯科蒂:“键盘。多么别致。”
——《星际迷航4:抢救未来》
随着“Siri”的出现,苹果公司用于(或多或少)通过语音控制 iPhone 的语音识别系统,通过语音控制设备或手机的想法一下子跃升到了“酷炫”的顶峰。当然,语音交互并不是什么新鲜事:哪个开发者不曾想象过像电影或漫画中的托尼·斯塔克那样与电脑对话;或者,如果您是更老派的科幻迷,您是否曾想象过您最喜欢的舰长与他的“企业号”对话?
幸运的是,Android 操作系统提供了一些内置的语音识别/语音转文本功能,并且访问它们相当直接。不幸的是,弄清楚如何以及何时使用这些功能仍然是一门艺术——例如,尝试知道何时开始监听用户语音并处理相关命令,可能是一个用户界面上的噩梦。如果您强迫用户先按屏幕上的按钮,您就会失去使语音驱动命令如此吸引人的“免提”功能;如果您只是从应用程序开始执行的那一刻起就开始监听音频输入,您不仅会遇到与 Android 活动生命周期交互的问题(当他们切换到其他应用时,您是否会继续读取输入通道?),而且几乎不可能区分是应用程序的命令,还是房间里其他人的命令(或者高速公路上的其他汽车的命令)。
事实上,有一个相当简单的信号可以知道用户何时想开始使用应用程序:当他们拿起手机时,或者对许多用户而言,当他们拿起耳机并戴上时。事实上,后一个事件比前一个事件更强的信号——许多用户为了各种与语音输入无关的任务而拿起手机,而耳机实际上只有一个用途,那就是所有与音频输入或输出有关的用途。因此,如果我们能以某种方式获得用户正在开始使用耳机的信号,语音控制的应用程序就可以开始其音频流分析。或者,如果用户已经戴着耳机,用户可以按下耳机上的按钮来“开始”。
虽然这无疑很可能是众多功能中的第一个,但来自Plantronics 的 Voyager Legend UC® 耳机正是提供了这种信息。Plantronics 送了我一个用于探索的单元。
蓝牙、Android 与你
在我们讨论在 Android 设备上编程耳机时,我将假设读者熟悉 Android 编程的基础知识,这意味着 Activities、Handlers 和 Intents 应该不是陌生的概念。如果您还不熟悉 Android,那么它是一个相当容易上手的操作系统——网上有大量的教程,只需要对 Java 语言有基本的了解。
故事的简短版本是,一旦与 Android 设备配对(请注意,这可以是手机或平板电脑,这为与手机无关的应用程序打开了一些有趣的可能性),Voyager Legend 就会通过蓝牙将几条信息发送到配对的 Android 设备。这些事件范围从“don”和“doff”事件(表示用户“戴上”设备或“摘下”设备)到按钮按下事件,再到设备拾取的不同传感器响应。
因此,任何耳机感知型应用程序的第一步都是在您的应用程序中识别配对的耳机,并请求 Android 开始向您发送设备通过蓝牙接收的事件。在 Android 中,接收来自设备其他部分的事件是通过 `BroadcastReceiver` 完成的;这本质上是一个超类,其 `onReceive()` 方法接收所有发送给该 BroadcastReceiver 的消息(通过 `IntentFilter` 过滤)。一旦建立,就可以轻松地开始监视事件流,查找特定事件——通常是来自设备的蓝牙通知,除非我们谈论的是通用的“已配对”和“未配对”事件——并根据应用程序的需求做出反应。
Voyager Legend 耳机包含两个按钮,一个称为“语音”按钮,另一个称为“通话”按钮——正常使用时,它们分别用于与耳机对话(按下“语音”按钮,它会告诉你耳机的剩余通话时间,这很不错,如果你问我的话)和接听来电。然而,从开发者的角度来看,它们每个都只是一个按钮,我们可以根据需要重新利用它们。
设备发送到 Android 的数据以及从 Android 发送到应用程序的数据的确切性质,在Plantronics 文档中有描述。这是一组特定于Plantronics 设备的 कोड 和字符串,要弄清楚具体发送的是什么,可能很棘手。幸运的是,Plantronics 的布道师 Cary Bran 在Plantronics 开发者论坛上编写了一些示例代码,提供了一个示例“事件流”应用程序,演示了发送的不同事件,并提供了两个类:`PlatronicsReceiver`(继承自 `BroadcastReceiver` 的类)和一个简单的消息包装器 `PlantronicsXEventMessage`。有关详细信息,请参阅http://developer.plantronics.com/blogs/Cary/2012/11/26/plugging-into-plantronics-headset-sensor-events-via-android。
事件,请
接收来自 Android 操作系统的事件,例如蓝牙事件,涉及使用 `BroadcastReceiver` 派生类,并且必须向 Android 操作系统注册,以便操作系统知道发送事件(Intent 对象)给 `BroadcastReceiver`。此注册可以有两种形式——一种是 `BroadcastReceiver` 存在于您的 Android 应用程序进程之外,要求在 AndroidManifest.xml 文件中注册 `BroadcastReceiver`;另一种是 `BroadcastReceiver` 在 `registerReceiver()` 方法中传递,这意味着 `BroadcastReceiver` 仅在您的 Android 进程运行时接收事件,并且不需要清单条目。因此,应用程序需要做的第一件事是创建一个这样的自定义 `BroadcastReceiver`(下面的 `PlantronicsReceiver`)并为其注册传入的蓝牙事件。
private void initBluetooth() {
handler = new BluetoothHandler();
receiver = new PlantronicsReceiver(btHandler);
intentFilter = new IntentFilter();
intentFilter.addCategory(
BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY +
"." +
BluetoothAssignedNumbers.PLANTRONICS);
intentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED);
intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
intentFilter.addAction(
BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT);
intentFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
intentFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
registerReceiver(receiver, intentFilter);
}
除了 `IntentFilter` 的蓝牙特定部分之外,这是一个非常正常的 `BroadcastReceiver` 实现。IntentFilter 中的操作指定了 BroadcastReceiver 将对哪些操作开放接收(我们不想被设备上发送的每个消息淹没),然后将其传递给 `registerReceiver()`,使 `PlantronicsReceiver` 开始工作。
(`PlantronicsReceiver` 如何解包通过蓝牙发送的信息包的详细信息实际上超出了本文的范围,但从 Cary 发布的代码中进行逆向工程并不难。简而言之,所有信息都打包在 Intent 中,附加数据作为该 Intent 中的“事件附加项”,`PlantronicsReceiver` 解包“事件附加项”以发现额外信息,例如按下了哪个按钮以及如何按下,并将这些信息设置为它创建的 `PlantronicsXEventMessage` 实例的属性。)
仅处理
请注意,`PlantronicsReceiver` 在其构造函数参数中接受一个 Handler——这是一个 Handler 扩展类,用于处理从 `BroadcastReceiver` 发送的消息。此 Handler 是消息的最终接收者,将由应用程序提供。应用程序在这里接收 `PlantronicsXEventMessage`,确定事件的类型(DON、DOFF、BUTTON 或其他任何类型),并提取事件附带的任何额外信息。例如,BUTTON 事件将附带三个额外属性:“`buttonId`”(描述按下了哪个按钮)、“`buttonName`”(上述按钮的名称)和“`pressType`”(指示注册的按下类型,短按或长按)。另一方面,BATTERY 事件将附带“level”(描述耳机的充电级别)、“charging”(一个“true”/“false”值,指示耳机是否已插入并正在充电)和“`minutesOfTalkTime`”(不言而喻)等属性。
`PlantronicsReceiver` 类是发送到 `PlantronicsXEventMessage` 的数据的最终仲裁者,请查看该代码以获取详细信息。
然后,Handler 将在其 `handleMessage()` 方法中接收这些消息对象,并像这样“解包”它们。
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case PlantronicsReceiver.HEADSET_EVENT:
PlantronicsXEventMessage message =
(PlantronicsXEventMessage) msg.obj;
// What is the type of this event?
String type = message.getEventType();
if (type.equals(PlantronicsXEventMessage.BUTTON_EVENT)) {
}
if (type.equals(PlantronicsXEventMessage.BATTERY_EVENT)) {
}
break;
default:
break;
}
}
请注意,Java 6 中新的“静态导入”功能可以用于减少这些类型检查的冗长。
录音机
例如,假设我们要创建一个类似数码录音机的应用程序。(40 岁以下的读者可能不知道,但在古代,当恐龙还在地球上漫步,设备必须通过电线连接才能运行时,“数码录音机”是一种设计用于将人类语音记录到某种存储介质上的设备——早期设备实际上使用蜡筒。)应用程序流程大致如下:如果应用程序正在运行,并且 Voyager Legend 已与设备配对,那么我们就等待一个“按钮”事件。
一旦按下按钮,我们就可以立即将设备切换到语音转文本模式,监听传入的音频流。
/**
* Handler for BluetoothReceiver events
*/
public class BluetoothHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case PlantronicsReceiver.HEADSET_EVENT:
PlantronicsXEventMessage message =
(PlantronicsXEventMessage) msg.obj;
// What is the type of this event?
String type = message.getEventType();
// If this is a "BUTTON" event, start the recorder
if (type.equals(PlantronicsXEventMessage.BUTTON_EVENT)) {
// Pop Toast
Toast.makeText(getApplicationContext(),
"Listening....",
Toast.LENGTH_SHORT).show();
Intent intent = new Intent(
RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
"en-US");
try {
startActivityForResult(intent, RESULT_SPEECH);
textPane.setText("");
} catch (ActivityNotFoundException a) {
Toast.makeText(getApplicationContext(),
"Ops! Your device doesn't support Speech to Text",
Toast.LENGTH_SHORT).show();
}
}
break;
default:
break;
}
}
}
当该 Activity 停止时,Android 将已收听音频流并对口语做出最佳猜测,返回一个 `ArrayList
@Override
protected void onActivityResult(int requestCode,
int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case RESULT_SPEECH: {
if (resultCode == RESULT_OK && null != data) {
ArrayList<String> text =
data.getStringArrayListExtra(
RecognizerIntent.EXTRA_RESULTS);
textPane.setText(text.get(0));
}
break;
}
}
}
从 Intent 中检索到数据后,很容易将其内容设置到应用程序的 TextView(`textPane`)上。
从这里开始,很容易想象这个应用程序如何保存笔记、加载其他笔记等等,包括用于执行上述所有操作的语音命令,但这只是基础。
摘要
读者可能会对实际的代码量感到惊讶——`PlantronicsReceiver` 解释蓝牙数据并向我们提供易于使用的消息对象,而 Android 操作系统负责将语音翻译成文本的“繁重工作”,我们有了一个相当实用的应用程序,仅需三个类(不包括 Cary 编写的代码)大约 150 行代码。这对于相对较小的工程投入来说,是一项相当大的功能,至少让我微笑着期待现代移动应用程序的使用方式。尽情享受吧!