应用程序的同步音量控件






4.57/5 (14投票s)
使用 WinMM.DLL 构建与系统音量控制实用程序同步的音量控制器。
引言
在即将发表的一篇文章中,我将介绍一款具有一些不寻常功能的 DirectSound 基础的波形播放/录音器。
GUI 将包含一个简单的播放音量控制器。由于我希望该控制器与系统音量控制实用程序同步,因此我需要使用 *WinMM.DLL* 函数,并且我认为这篇介绍这些函数如何使用的过渡性文章,特别是展示如何将此类控制器与系统音量控制同步,可能会引起大家的兴趣。
演示应用程序的功能
我将展示两个控件——声卡输入音量控制(线路输入)和输出音量控制(扬声器)。从编程角度来看,它们被同等对待,因此当我谈论“控件”时,您应该理解我说的话同样适用于输入和输出控件。
这里的一切都与其他推子控件直接相关,例如低音、高音、独立的左右声道控件等——包括外部捕获设备,例如网络摄像头的麦克风。
您还将看到如何静音/取消静音这些控件(如果它们可以被静音),而不会干扰设置,正如您在此屏幕截图中所看到的

同步要求音量控件的变化能够准确地反映在系统音量控制中,反之亦然。线路的静音和取消静音也是如此。
这些控件是准对数(logarithmic)的(就像系统实用程序中的控件一样),这意味着连续的音量步进上下遵循近似对数定律的规律。这是因为我们感知声音级别变化的方式。最好是让每个步进都代表一个 appena 可察觉的级别增加(或减少),而这里显示的控件正是这样工作的。事实上,您的键盘按钮也是这样设计的。
一个简单的实验
您可能想和您的声卡一起进行以下实验。同时启动演示应用程序和系统音量控制实用程序。
现在将声卡的扬声器音量降低到零(不要通过静音,而是将扬声器控件上的滑块拖到最低位置),然后使用键盘上的增大音量按钮,逐步浏览控件的全部范围,同时注意演示屏幕上的音量级别。
您应该会发现有 25 个步进,并且它们非常接近源代码中显示的数组值,该数组值来自我自己的键盘。
int[] volSteps = { 0, 2621, 5242, 7863,
10484, 13105, 15726, 18347,
20968, 23589, 26210, 28831,
31452, 34073, 36694, 39315,
41936, 44557, 47178, 49799,
52420, 55041, 57662, 60283,
62904, 65535 };
在此过程中,您将看到演示扬声器控件和系统扬声器控件同步移动。完成后,移动系统音量控制中的线路和扬声器滑块,您将看到演示控件跟随。
演示扬声器控件旁边的 +/- 按钮,当单击时,将匹配按下键盘上的向上和向下按钮,从而提供 appena 可察觉的音量增加或减少。步进将对应于 `volSteps` 数组值(最低有效位将漂移)。否则,您可以单击刻度上的任意位置来更改音量,您将转到与您单击刻度位置相对应的音量级别。系统控件将始终反映这些更改。
演示屏幕
演示屏幕背后是一段相当复杂的代码,或者对于那些以前没有接触过这些 WinMM 函数的读者来说,可能会显得如此。嗯,即使是有过接触的人也可能会这么认为。
我选择了这两个特定的控件——线路输入和扬声器输出——因为它们恰好对应于我的桌面,我在那里通过声卡收听广播,广播的低阻抗输出连接到线路输入。因此,在代码开发过程中,测试代码一直很容易。通过使用此代码,您可以替换或添加任何适合您目的的其他控件。
为了使内容更容易理解,我将在此只展示线路输入控件。我所说的关于该控件的一切同样适用于扬声器控件。
图形非常简单。刻度只是一个 ASCII 标签,条形是一个细长的矩形。刻度的非线性外观仅用于暗示对数刻度——不应认为它基于任何数学原理。
如前所述,音量级别的更改不是通过拖动指针来完成的,而是通过单击刻度上的任意位置来完成的。您也可以单击向上或向下按钮,每次单击都会将级别更改一个步进。
通过演示屏幕或系统音量控制发起的音量级别更改,会导致新的级别( 0 .. 65535 )出现在控件旁边的文本框中。我还将控件的静音状态显示为 Mute Volume,其值始终为 0 或 1。零表示未静音,Mute Volume 值将跟随静音复选框的选中或取消选中。
一些作者通过将音量滑块降低到零来静音控件,但这既不合适也不必要。
控件的静音状态的设置和读取方式与线路音量的设置和读取方式完全相同。
关于窗体绘制的一些说明
因为我很少使用动态图形,所以我忘了在窗体重绘时需要注意确保包含这些图形,例如在窗体最小化后恢复时。否则,您可能会想知道您的图形去哪儿了!
除非您有首选的实现方法,否则应遵循以下重写的 `OnPaint` 安排:
protected override void OnPaint( PaintEventArgs e )
{
base.OnPaint(e);
}
private void scaleLine_Paint(object sender, PaintEventArgs e)
{
Graphics gLine = e.Graphics;
gLine.FillRectangle(brushBlue, rectX, rectY, rectWLine, rectH);
}
private void Form1_Load( object sender, EventArgs e )
{
//
scaleLine.Paint += new PaintEventHandler(this.scaleLine_Paint);
//
}
更新条形需要先擦除现有条形,然后再绘制新条形。为此,我有一个 `eraseBrush`,其颜色与窗体的 `BackColor` 相同。当音量级别更改时,以下代码会将新的音量级别转换为填充矩形:
rectW = (int)((newVol / 65535.0) * rectWMax + 1);
gLine.FillRectangle(brushErase, rectX, rectY, rectWMax + 1, rectH);
gLine.FillRectangle(brushBlue, rectX, rectY, rectW, rectH);
这就是图形处理所需的全部。无论出于何种原因,每次重绘窗体时,原本可能丢失的条形都会被重绘。因为控件在零音量(即没有条形)时看起来会很奇怪,所以我安排条形在零音量或接近零音量级别时保持可见。
MM 类
MM 类包含了我们与控件交互所需的一切。
为了便于理解,我在 MM 类中只包含了演示所需的常量和导入。否则,会有大量令人费解的常量和函数需要您去梳理,其中一些使用过,大部分没有。更正式的介绍会包含我省略的内容,如果您觉得有必要,我将提供参考,说明在哪里可以充实此类。
该类使我们能够获取和设置音量级别,获取和设置静音状态,这几乎是相同的操作,当然还有确保我们能够访问包含我们控件的混音器。一旦清楚要传递哪些参数以及如何将它们传递给 WinMM 函数,其余的就相对容易了。
您会注意到我只处理默认混音器。如果您想访问系统中的其他混音器(声卡),您可以轻松做到,尽管我需要指出,对于除此处处理的同步任务之外的其他目的,DirectSound 更易于使用和更直观。混音器从零开始索引,默认混音器的 `DeviceID` 为零。
与系统音量控制实用程序同步
需要考虑如何将一个控件与其在系统实用程序中的对应控件同步,因为实现这一点的方式不止一种。我使用的是 `MM_MIXM_CONTROL_CHANGE`(= 0x3D1)消息,它指示某个控件已更改,包括线路静音状态的任何更改。
识别到此消息后,我们可以选择更新所有控件,或者更好的是,像这里一样,只更新已更改的控件。消息的 `LParam` 是控件的唯一 ID(`dwControlID`,`MIXERCONTROL` 结构的一个成员)。在初始化期间,该结构会依次被引用每个控件,因此在处理消息时,我们将知道它来自哪里,并且能够仅更新一个控件,而开销非常小。
为了能够拦截该消息,我们需要创建一个窗口,消息可以被定向到该窗口并进行测试。`NativeWindow` 子类提供了一个简洁的解决方案,并且其使用有很好的文档记录。每当检测到 `MM_MIXM_CONTROL_CHANGE` 消息时,就会将更新发布到屏幕。
`SubclassHWND` 直接来自 MSDN。
using System.Windows.Forms;
namespace SynchronizedVolumeControl
{
public class SubclassHWND : NativeWindow
{
protected override void WndProc( ref Message m )
{
base.WndProc( ref m );
}
}
}
将在 `Form1_Load` 期间声明将拦截 `MM_MIXM_CONTROL_CHANGE` 消息的窗口。
// Set up a window to receive MM_MIXM_CONTROL_CHANGE messages ...
SubclassHWND w = new SubclassHWND();
w.AssignHandle(this.Handle);
int iw = (int)this.Handle; // Note that the window's handle needs to be cast as
// an integer before it can be used
// ... and we can now activate the message monitor
bool b = MM.MonitorControl( iw );
... 这样监控就开始了。
这是 `MonitorControl` 函数:
public static bool MonitorControl( int iw ) // iw is the window handle
{
int rc = -1;
bool retValue = false;
int hmixer;
rc = mixerOpen(
out hmixer,
0,
iw,
0,
CALLBACK_WINDOW);
return retValue = (MMSYSERR_NOERROR == rc) ? true : false;
}
检测到 `MM_MIXM_CONTROL_CHANGE` 消息会触发以下代码,该代码会更新图形和复选框,从而使演示控件及其系统对应项保持同步。
protected override void WndProc( ref Message m )
{
if (m.Msg == MM.MM_MIXM_CONTROL_CHANGE) // Code 0x3D1 indicates a control change
{
int i = (int)(m.LParam);
// We can't use switch so we must do it this way:
bool b1 = i == lineVolumeControlID ? true : false;
bool b2 = i == lineMuteControlID ? true : false;
//
if (b1)
{
// Line volume update LINE VOLUME
int v = MM.GetVolume(lineVolumeControl, lineComponent);
tbVolLine.Text = v.ToString();
rectWLine = (int)((v / 65535.0) * rectWMax) + 1;
// This will prevent the volume bar from disappearing at near zero levels
rectWLine = rectWLine < 4 ? 4 : rectWLine;
gLine.FillRectangle(brushErase, rectX, rectY, rectWMax + 1, rectH);
gLine.FillRectangle(brushBlue, rectX, rectY, rectWLine, rectH);
}
if (b2)
{
// Line mute update LINE MUTE
int muteStatus = MM.GetVolume(lineMuteControl, lineComponent);
cbLine.Checked = muteStatus > 0 ? true : false;
tbMuteLine.Text = muteStatus.ToString();
}
//
}
// The intercepted message, with all other messages is
// now forwarded to base.WndProc for processing
base.WndProc(ref m);
}
在 `Form1_Load` 期间,一个 `MM.CheckMixer()` 函数(我未在此展示)会尝试打开,然后关闭默认混音器,如果失败则中止加载。
`MM.MonitorControl(iw)` 函数为我们想要拦截的消息设置了一个报告机制。`CALLBACK_WINDOW` 和 `iw` 参数将 `NativeWindow` `w` 设置为接收感兴趣的消息。
读者会理解,如果您打算构建一个具有与系统音量实用程序相同功能的独立实用程序,您当然会做更多的事情。您首先需要确定您的声卡的输入和输出功能范围,并且您的编码将与您的发现和您想要包含的内容保持一致。
我在这里只关注展示如何使用 WinMM 函数以及如何实现同步。
结论
我从研究这个小型项目,特别是从论坛上的提问中得知,外面有很多关于 *WinMM.DLL* 如何使用的不确定性,我希望我已经能够消除一些神秘感,并鼓励读者自信地使用这些函数。
参考文献
我推荐以下读物给对这个主题感兴趣的初学者。第一篇参考是对混音器及其控件的冗长但权威的论述,值得仔细阅读。这个列表只是可用资源的一小部分。