65.9K
CodeProject 正在变化。 阅读更多。
Home

实用的录音机,带声音激活

2014 年 9 月 3 日

CPOL

12分钟阅读

viewsIcon

96189

downloadIcon

3918

这款录音机的便利性在于其简洁性和对目的的理解。

 

目录

1 背景

软件开发人员经常被指责缺乏对应用程序领域的理解,并且视野狭窄,只从“编程”的角度看问题。这公平吗?有时这反映了指责软件开发人员的人的无知,但在某些情况下,这是事实。所有狭窄的专家都会受到限制。找到一个好的软件开发实用工具比在一个狭窄的应用领域找到任何面向领域的工具都要容易。有人说,最好的软件是为自己消费而开发的软件。我认为这是真的。

就拿那些录音机来说吧,几乎所有这些。我不是指那些作为一些复杂的声音编辑器一部分的录音机。它们可以做得非常出色:可视化、各种即时过滤等等。我指的是像大多数录音机那样简单的东西,比如 Windows 自带的那一个。如果你试图将其用于任何实际目的,你会发现作者从未尝试过为与测试该软件无关的任何实际目的而录制任何东西。

一个可以立即发现该工具无法使用的人,可能是,比如说,一个音乐家,他想录制排练来听听效果并修复问题。一个想在会议上记录头脑风暴的人也会发现问题,但让我们回到音乐家。

音乐家双手拿着萨克斯管。小提琴……吉他……这个人不想伸手去拿电脑,也不想费太大劲来完成每一次录音。这个人想再次演奏这首曲子,因为上一首几乎完美了,但琴弦稍微从手指滑落,所以那个标志性的出色弯音没有预期的那么动人和感人。所以这首曲子应该重复一遍。一遍又一遍……

声音激活的需求显而易见且广为人知,但这不仅仅是关于声音激活。点击鼠标……操作文件浏览器对话框并为下一个录音输入文件名,而不知道在哪个目录中。一遍又一遍地点击“录制”、“停止”和“保存”按钮。得了吧。

让我们看看是否可以从一个录制大量样本的人的角度,而不是从工具开发者的角度,以一种实用的方式来完成它。

2 操作

实际上,这没什么复杂或困难的。这只是常识问题。实际上非常简单。首先,通过一些适当的选项设置,只需一次按键即可停止和保存录音,即使用宽大且易于触及的空格键。它(同样,可选地)会重新开始录制并保存之前的录音。因此,文件名永远不会被输入。它们是基于预先定义的基文件名和顺序号生成的,但方式可以防止覆盖任何先前写入的文件。

然后,关于可选的声音激活呢?它由声音音量指示器控件上的一个阈值确定。这是可视化的。当声音音量超过垂直条(该条设置为正无穷大且在启动时不可见,但可以移动到音量指示器上)时,录音就会被激活。状态通过闪烁的指示器、显示“纯”(无暂停)经过时间的数字手表以及按钮的 `IsEnabled` 状态来显示。

总之,就是这样。操作已在应用程序的帮助屏幕上描述;只有精细的细节,因为基本操作是自explanatory 的。顶部的图片显示了“关于”对话框和主窗口的最小展开视图,它将所有控件集中在一处。当两个 `Expander` 控件都展开时,它看起来是这样的。

这两个 `Expander` 控件显示了不经常更改且在重复录制期间不需要显示的选项。请注意,声音激活阈值保持方便,因为可能需要多次调整它。

顶部展开器下面的重要声音激活选项显示了一个重要参数:检测到激活声音与激活录音之间的时间延迟。这是为了将用于激活的声音从录音中剪掉。这种技术效果相当好,默认值 200 毫秒效果也算不错,至少作为初步近似。

问题是:为什么不声音去激活,通过一段静默来去激活?我对此表示怀疑,但还是试了试。不行,这完全不切实际,会导致令人沮丧的误报。另一个理由是不这样做:从结尾处剪掉不需要的部分会更简单快捷。<</p>

语音识别用于激活和去激活怎么样?很有可能;我试过(不是在这个应用程序上)。这可能是一种品味问题,但我认为这也不切实际:即使是很少的误报和漏报也会非常恼人。也许对于录制会议来说它会更好。欢迎任何人尝试。

对我来说,便利性首先是极简主义。没有什么应该分散对主要事物的注意力。各种文件类型呢:MP3、AAC、WMA 等等?不。如果你真的想做好录音,你不会信任嵌入式编码器。谁知道它们会做什么?你想要 WAV,然后以你想要的方式压缩它们,只有一些选定的文件。同样,功能集是讨论的重点,但我有相当严格的优先级。当然,任何批评和建议都一如既往地受到欢迎。

我还想稍微讨论一下首选项的持久性。它们可以保存在文件中,这很清楚。在“用户”目录中为用户预定义的区域?不。我认为创建尽可能少地在目标系统上留下痕迹且不需要安装的应用程序很重要。用户将文件保存在自己选择的目录中,之后可以加载首选项。重要的是,基文件名可以包含目录“.\”,这意味着文件可以保存在任何当前目录中,无论它是什么。

首选项可以自动加载吗?当然。它按照以下优先级发生:

  1. 如果在命令行中给出了首选项文件名并且该文件存在,则加载该文件。方便创建批处理文件,以便使用不同的首选项启动应用程序。
  2. 否则,如果在应用程序启动前在工作目录中找到名为“Default”的文件,则加载该文件。
  3. 否则,如果在应用程序的可执行目录中找到名为“Default”的文件,则加载该文件。

这些规则提供了最大的灵活性。

3 关键实现细节

由于工具的简单性,实现非常简单。这是录音的核心实现,基于“winmm.dll”和 P/Invoke。

using System;
using System.Text;
using System.Runtime.InteropServices;
using System.Threading;

internal static class Mci {

    static class DefinitionSet {
        internal const string DllName = "winmm.dll";
        internal const string LevelMeterDeviceId = "soundLevelMeterDevice";
        internal const string SoundRecordDeviceId = "soundRecordDevice";
        internal const string OpenCommandFormat =
            "open new type waveaudio alias {0}";
        internal const string StatusLevelCommandFormat = "status {0} level";
        internal const string RecordCommandFormat = "record {0}";
        internal const string PauseCommandFormat = "pause {0}";
        internal const string StopCommandFormat = "stop {0}";
        internal const string CloseCommandFormat = "close {0}";
        internal const string SaveCommandFormatFormat =
            @"save {0} ""{{0}}""";
        internal static readonly string OpenLevelMeterCommand =
            string.Format(OpenCommandFormat, LevelMeterDeviceId);
        internal static readonly string OpenRecorderCommand =
            string.Format(OpenCommandFormat, SoundRecordDeviceId);
        internal static readonly string StatusLevelCommand =
            string.Format(StatusLevelCommandFormat, LevelMeterDeviceId);
        internal static readonly string RecordCommand =
            string.Format(RecordCommandFormat, SoundRecordDeviceId);
        internal static readonly string PauseCommand =
            string.Format(PauseCommandFormat, SoundRecordDeviceId);
        internal static readonly string StopCommand =
            string.Format(StopCommandFormat, SoundRecordDeviceId);
        internal static readonly string CloseRecorderCommand =
            string.Format(CloseCommandFormat, SoundRecordDeviceId);
        internal static readonly string CloseLevelMeterCommand =
            string.Format(CloseCommandFormat, LevelMeterDeviceId);
        internal static readonly string SaveCommandFormat =
            string.Format(SaveCommandFormatFormat, SoundRecordDeviceId);
        internal const int ReturnNumDigits = 0x10;
        internal const int MaximumLevel = 128; //why?
    } //DefinitionSet

    internal class MciException : ApplicationException {
        internal MciException(long mciErrorCode) :
            base (string.Format("MCI error {0}", mciErrorCode)) {
                this.MciErrorCode = mciErrorCode; }
        internal long MciErrorCode { get; private set; }
    } //class MciException

    [DllImport(DefinitionSet.DllName)]
    private static extern long mciSendString(
        string strCommand, StringBuilder strReturn, int iReturnLength, IntPtr oCallback);

    internal static void StartLevelMeter() {
        mciSendString(DefinitionSet.OpenLevelMeterCommand, null, 0, IntPtr.Zero);
    } //StartLevelMeter

    internal const double MaximumLevel = DefinitionSet.MaximumLevel;
    internal static double GetLevel(int count, out double maxLevel, int delayMs) {
        double buf = 0;
        maxLevel = double.NegativeInfinity;
        for (int index = 0; index < count; ++index) {
            StringBuilder sb = new StringBuilder();
            mciSendString(DefinitionSet.StatusLevelCommand,
                sb, DefinitionSet.ReturnNumDigits, IntPtr.Zero);
            double value;
            if (!double.TryParse(sb.ToString(), out value))
                return 0;
            buf += value;
            if (value > maxLevel) maxLevel = value;
            System.Threading.Thread.Sleep(delayMs);
        } //loop
        return buf / count;
    } //GetLevel
    internal static double GetLevel() {
        double dummyMax;
        return GetLevel(1, out dummyMax, 0);
    } //GetLevel
    internal static void CloseLevelMeter() {
        mciSendString(DefinitionSet.CloseLevelMeterCommand, null, 0, IntPtr.Zero);
    } //CloseLevelMeter

    internal static void Open() {
        mciSendString(DefinitionSet.OpenRecorderCommand, null, 0, IntPtr.Zero);
    } //Open

    internal static void Record() {
        mciSendString(DefinitionSet.RecordCommand, null, 0, IntPtr.Zero);
    } //Record

    internal static void Pause() {
        mciSendString(DefinitionSet.PauseCommand, null, 0, IntPtr.Zero);
    } //Pause

    internal static void Stop() {
        mciSendString(DefinitionSet.StopCommand, null, 0, IntPtr.Zero);
    } //Stop

    internal static void Close() {
        mciSendString(DefinitionSet.CloseRecorderCommand, null, 0, IntPtr.Zero);
    } //Close

    internal static void SaveRecording(string fileName) {
        mciSendString(
            string.Format(DefinitionSet.SaveCommandFormat, fileName), null, 0, IntPtr.Zero);
    } //SaveRecording

} //class Mci

现在,这是想法:字符串 MCI 命令有一个名为“device ID”的参数。它们代表某种“逻辑设备”。多个逻辑设备可以在同一个通道上运行。通道本身的选定依赖于系统选定,例如,可以使用操作系统音量混音器完成。特别是,在 Windows 7 中,它通过“录音设备”配置窗口进行选择。

我打开了两个设备,一个用于音频“电平表”,另一个用于录音;请参阅字符串命令 `DefinitionSet.Open*Command` 和 `DefinitionSet.Close*Command`。可以通过调用 `GetLevel` 方法在 UI 中显示音频电平。是的,这肯定会占用 CPU,意味着轮询,拉技术,而且效率不高,但使用此 API 没有其他方法。因此,应谨慎使用该应用程序,因为它会永久占用系统 CPU 资源。

录音命令直接在 UI 中使用;这在文件“WindowsMain.Actions.cs”中显示。

3.1 条形指示器

音量电平在称为 `StripIndicator` 的自定义控件中显示。此控件有一个有趣的方面:许多,许多开发人员曾在网上询问过是否有类似的东西。出于某些有趣的原因,他们试图基于 `System.Windows.Controls.ProgressBar` 类来实现。问题是该控件烦人的动画,人们试图删除它但失败了。这个“有趣的原因”当然是“思维惯性”。人们认为如果某些东西看起来像他们想要的东西,就应该对其进行一些修改。

但这并非 WPF 的工作方式。更有趣的事实是,找到了基于 `ProgressBar` 的解决方案,但它们相当复杂,几乎从头开始在 XAML 中定义了整个控件样式。同时,使用简单的原始控件 `System.Windows.Shapes.Rectangle` 创建类似控件几乎是微不足道的。此外,它更灵活,因为对于这个特定的应用程序,我还为语音激活录音所需的音量级别需要一个指示器。

我开发了这个控件的一个单独的、更通用的版本,使其成为 XAML 中真正的一等公民,它需要正确使用“依赖项属性”。在这个应用程序中不需要它,并且是另一篇文章的主题。我将尝试发布它,因为,如网络搜索所示,此类控件需求量很大。:-)

请参阅我根据本文撰写的文章:WPF ProgressBar 动画问题:更清晰的解决方案

3.2 使用字符而非位图

WPF 图标或图像(窗口内容中的字形)比其他 UI 库中的类似元素更灵活。例如,`MenuItem` 的图标或 `Button` 的某些上下文可以是任何类型的对象。

我想到使用……代表这些字形的 Unicode 字符。它们可以完美地呈现为“图标”或“图像”对象,并可以代替“真实”位图使用。在此应用程序中,除了应用程序图标(见下文)之外,没有一个位图或图标。

这带来了巨大的好处:摆脱了作者对某些图像的版权问题。确实,在应用程序中添加图像可能很麻烦:它们应该是原创的,或者受某种许可证的保护。但是如何创建原创图像,例如,著名的“停止”、“暂停”或“播放”字形?相反,使用系统字体实现的字符受到管理软件开发的通用法律的约束。我们永远不需要为文本输出征求许可。

3.3 Logo 的矢量图形

在我的一些回答中,我提出了我制作矢量图形的便捷且可维护的方式。这是想法:图形在非常轻量级且功能强大的开源应用程序 InkScape (http://en.wikipedia.org/wiki/Inkscape, http://www.inkscape.org/) 中创建。

它允许将图形以 XAML 文件保存为 `Canvas` 元素。XAML 输出经过手动编辑,以缩放到父 `UIElement` 并放入任何资源字典中。(应删除名称,并为顶层 `Canvas` 元素添加字典键。)然后,该元素可以在任何窗口或自定义控件中使用,以呈现动态可缩放的矢量图形。此外,通过访问顶层 Canvas 元素的内部元素,可以轻松地用于 WPF 动画或交互行为。

3.4 一些有趣的特性

这是我第一次决定添加有效的 Windows 应用程序图标,同时也用作窗口图标。图标尺寸为 16x16、32x32、64x64、48x48、96x96 和 128x128。其中大多数尺寸确实在系统 Shell 的不同文件视图中使用,并以无损缩放的方式显示。我只使用了 24 位像素格式。这是应用程序中唯一的图标;其他所有内容都使用了字符(见上文)或纯矢量图形(产品 Logo 在“关于”对话框中显示为 XAML 矢量图像。尽管如此,这个单一图标就占用了 310,398 字节,而整个可执行文件,包括此图标,仅占 672,256 字节。也就是说,单个图标占用了可执行文件大小的 46% 以上。

不用说,相同的矢量图形图像,可以自动渲染到任何尺寸,占用的内存要少得多:大约 2K,尽管它以文本形式嵌入在“关于”对话框的 XAML 中。

这告诉我们什么?嗯:Windows API 和 Shell 的大部分是相当古老的:没有矢量图形,甚至没有压缩。

4 应用程序构建和兼容性

该工作目标是尽可能最低的 .NET 版本:v.3.5。(甚至可以在 v.2.0 上完成,但我选择了使用 WPF。)它不需要任何其他库,甚至不需要 DirectSound,没有任何可能被认为是可选的且可能缺失的东西。

可以使用 Visual Studio 等工具将解决方案转换为任何更高版本。

构建解决方案不需要 Visual Studio 或其他任何东西;.NET 框架本身就足够了。使用批处理文件 build.3.5.bat 即可一键完成构建。

5 未来工作

坦白说,我不能保证我一定会继续这项工作。只有当我需要更复杂的设置时,我可能会这样做。特别是,它将拥有多个输入通道,并且不同的通道可以同时工作。在这种情况下,录音机肯定需要在应用程序中选择通道,配置通道之间的音量平衡,并且很可能还需要更多功能,例如输入端的选择性过滤,不同通道有不同的过滤。等等……

为应用程序添加任何复杂性肯定需要更好的 API,而不是我上面描述的过时的、蹩脚的 API。我认为第一个候选者是DirectSound。此外,它还可以为在永久保存到磁盘之前播放录制的样本提供合理的方法,以及更多功能。

6 结论

目前,该应用程序可以仅仅替代 Windows 自带的录音机:功能几乎一样极简,但是……可用。:-))

© . All rights reserved.