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

使用 OpenTK/OpenAL 开发跨平台 DIS VOIP 应用

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (8投票s)

2010年2月28日

BSD

10分钟阅读

viewsIcon

49925

downloadIcon

1734

该应用程序利用分布式交互仿真协议 (IEEE 1278.1) 进行语音通信 (VOIP)

引言

本应用的目的是提供一个示例,说明如何在 Mono 环境下使用 OpenAL (通过 OpenTK) 实现利用分布式交互仿真 (DIS) IEEE 标准 1278.1 进行语音通信。撰写本文的动机是我一直在寻找这两种技术的相关示例。由于在互联网上关于使用 OpenAL (.NET Framework 下) 和 DIS 的示例很少,我感觉提供这个示例可能对其他开发者有所帮助。请注意,这项技术类似于使用网络语音电话 (VOIP),但不使用“会话控制”来处理呼叫,因此该应用程序只是通过互联网传输编码后的语音数据。

背景

Open Audio Library (OpenAL) 是一个跨平台的 3D 音频 API,许多游戏开发者过去曾成功使用过它。Open Tool Kit (OpenTK) 的开发者为 OpenAL API (以及 OpenGL 和 OpenCL) 提供了一个包装器,从而提供了与任何 Mono/.NET 语言交互的功能。

分布式交互仿真 (DIS) 或 IEEE 1278.1 自 1995 年以来就存在,主要由军事组织用于进行仿真。选择此协议是因为 MOVES 研究所的研究工作。海军 Post 研究生 学校在开发 Open-DIS 实现方面。作为该项目 C# 部分的贡献者,我的目的是利用已有的工作成果,创建一个同时包含 OpenAL 和 Open-DIS 的产品。

代码描述

提供的代码仅展示了 OpenAL API 在捕获和传输音频到互联网的一种用法。该应用包含四个模块和一个主窗体应用程序。

  • AudioIn:包含初始化、启动和停止麦克风所需所有方法的项目
  • AudioOut:包含初始化和播放音频数据所需所有方法的项目
  • DISNET:包含用于处理 DIS 协议数据单元 (PDU) 的方法的项目,特别是 Transmitter 和 Signal PDU
  • Sockets:包含套接字通信架构的项目。提供的实现仅使用 UDP 广播。
  • OpenDISRadioTransmitter:包含主窗体的项目,它封装了所有其他类,以提供一个简单的收音机界面。在此描述中,“收音机”指的就是这个窗体/应用程序。这里的收音机有一个发射器和多个接收器。请注意,故意设计了一个简化的 GUI 界面,因为本项目的主要目标是介绍 OpenAL 和 DIS。

RadioTestSuite 窗体

这是生成 GUI 的主窗体,也是所有初始化的起点。所有初始化都从 RadioTestSuite 构造函数开始。

public RadioTestSuite()
{
	InitializeComponent();
	InitializeTimer();
	InitializeRadioCommunications();
	InitializeRadioReceiversTransmitters();
}

InitializeTimer()

此方法启动一个 StopWatch,用于设置所有出站 PDU 的时间戳。时间戳数据在此应用程序中未使用,仅为完整性而提供。

InitializeRadioCommunications()

此方法设置用于 UDP 广播接收和发送的套接字。选择了一个随机端口 9737。在此方法中,为发射器属性提供了默认设置。在 DIS 中,Transmitter PDU 包含发射无线电的频率以及无线电是处于开启状态、开启并正在发送,还是开启但未发送状态。每个接收无线电都将此信息存储在一个集合中,以确定是否应播放音频(如果其频率与接收器的设置相匹配)。由于每个无线电应该是唯一的,并且只有一个发射器,因此 Entity ID 被设置为包含其运行系统 IP 地址的最后一个八位字节。因此,如果在同一台机器上运行这两个应用程序,将会发生冲突,除非更改 RadioID EntityID 。Transmitter PDU 每 5 秒广播一次(心跳),以允许所有接收器更新该无线电发射器的状态。

Signal PDU 也设置了一些默认值。在 DIS 中,Signal PDU 包含实际的数字音频语音数据。Signal PDU 包含采样率、采样数和编码类型以及编码后的音频。IEEE 标准中概述了几种编码方案,但在本示例应用程序中,仅使用了 uLaw(由 LumiSoft 提供实现)。

最后,初始化麦克风和音频输出功能。麦克风设置为读取 Mono16 格式,因此一次读取 2 个字节的数据,这使得缓冲区大小是采样数的两倍。

StartMicrophone(audioSamplingRate, microphoneGain,
AudioCapture.DefaultDevice, ALFormat.Mono16, numberOfSamples * 2); 

InitializeRadioReceiversTransmitters()

此方法扫描窗体控件,直到找到与“Radio”匹配的用户控件。找到后,会为其提供一个 uniqueID Name 。此 uniqueID 将用于标识哪个 Radio 控件被按下,以设置频率和扬声器选择。

麦克风

OutputMicrophoneSoundToSignalPDU() 方法是一个单独的线程,用于轮询麦克风以获取数据。选择此方法是为了减轻停止和启动麦克风的开销。在此方法中,如果按下窗体上的“Transmit”按钮 (isPTTActivated = true),则会发送一个 Transmit PDU,指定“TransmitterOnTransmitting”,然后收集到的任何麦克风数据将通过 Signal PDU 发送出去。使用队列来收集任何麦克风数据。

private void OutputMicrophoneSoundToSignalPDU()
{
	while (continueSendingMicrophoneData)
	{
		if (microphone.MicrophoneData.Count > 0)
		{
			//Remove data from the Queue
			byte[] raw = microphone.MicrophoneData.Dequeue();

			//If PTT is true then data will be sent out on the PDU
			if (isPTTActivated == true)
			{
				byte[] encodedData = uLaw.Encode(raw, 0, raw.Length);
				raw = uLaw.Decode
					(encodedData, 0, encodedData.Length);
				signalPDU.ExerciseID = transmitterPDU.ExerciseID;
				signalPDU.EntityId = transmitterPDU.EntityId;
				signalPDU.RadioId = transmitterPDU.RadioId;
				signalPDU.Data = encodedData;
				signalPDU.Samples = System.Convert.ToInt16
							(encodedData.Length);
				signalPDU.Timestamp = 
					TimeStamp(TypeTimeStamp.Absolute);
				DISnet.DataStreamUtilities.DataOutputStream ds2 =
				new DISnet.DataStreamUtilities.DataOutputStream
				(endianType);
				signalPDU.marshalAutoLengthSet(ds2);
				sendData.BroadcastMessage(ds2.ConvertToBytes());
			}
		}

		Thread.Sleep(1);
	}
}

音频输出

代码的音频输出部分发生在从套接字接收到数据包时。如果它是 Transmitter PDU,则会将相应的频率和 ID 存储在一个集合中。当后续的 Signal PDU 到达时,如果其 ID 和频率与 Transmitter 集合中的匹配,则播放音频。在本应用程序中,用户可以选择将声音输出到哪个扬声器。这允许同时进行两种不同的对话(左或右)。

//Play back unencoded data
playAudio.PlayBackAudio(unEncodedData, ALFormat.Mono16,
(int)signalPDU.SampleRate, retrievedFreqSpeakerTransmitterReceiver.SpeakerLocation);

音频的实际播放由 OpenAL API 提供,列在以下代码片段中。这会使用 AL.GenBuffer() 为一个唯一的 int 生成一个缓冲区。然后使用 AL.BufferData(....) 将数据 (unencodedData) 复制到该缓冲区。然后使用 ALSource3f.Position 设置扬声器位置(请注意,如果使用环绕声扬声器系统而非耳机,由于 OpenAL 的 3D 特性,可能会有一些声音从中间扬声器发出)。加载完缓冲区后,播放音频并清除缓冲区。

/// <summary>
/// Playback the audio
/// </summary>
/// <param name="unencodedData">Raw byte data</param>
/// <param name="recordingFormat">OpenAL sound format</param>
/// <param name="sampleFrequency">Frequency of the samples</param>
/// <param name="speakerLocation">Speaker location</param>
public void PlayBackAudio(byte[] unencodedData, ALFormat recordingFormat,
	int sampleFrequency, SpeakerLocation speakerLocation)
{
	//Determine if sources needed to be switched
	if (sourcesLeft == 0)
	{
		sourcesLeft = sources.Length;
	}

	//Used to rotate the sources being used.
	sourcesLeft--;

	int buf = AL.GenBuffer();

	AL.BufferData(buf, recordingFormat, unencodedData, 
			unencodedData.Length, sampleFrequency);

	position = SetSpeakerPosition(speakerLocation);
	AL.Source(sources[sourcesLeft], ALSource3f.Position, ref position);

	AL.SourceQueueBuffer(sources[sourcesLeft], buf);
	if (AL.GetSourceState(sources[sourcesLeft]) != ALSourceState.Playing)
	{
		ClearSourcePlayBackBuffers(sources[sourcesLeft]);
		AL.SourcePlay(sources[sourcesLeft]);
	}

	ClearSourcePlayBackBuffers(sources[sourcesLeft]);
}

使用应用程序

在使用之前,必须安装以下库 OpenAL。对于 Windows,请使用名为“OpenAL installer for Windows”的版本。我在使用 SUSE Linux 测试时,提供的版本 1.10.622 存在问题/bug,因此我采取的步骤是安装 SUSE 的 YAST 中的 OpenAL,然后下载以下 OpenAL 源代码版本 1.11.753 并按照该网站上的说明进行编译。编译后,覆盖 Linux 安装的二进制文件。要在 Linux 上运行,需要版本 1.11.753 或更高版本。OpenTK 不需要安装,因为二进制文件已包含在此项目的源代码中。但对于您自己的开发,下载中附带了一些示例。对于开发环境,需要安装 Windows Framework (3.5 或更高版本) 或 Mono-Project 框架 2.6.1 或更高版本。要编译源代码,可以使用 Windows IDE (Express 或 Professional) 或 MonoDevelop 版本 2.2.1 或更高版本。请注意,MonoDevelop 支持 Windows 和 Linux,并且我曾用它在 openSUSE 上进行测试。安装好这些之后,就可以编译和运行提供的测试应用程序了。在 Linux 上使用 USB 耳机时存在一个问题,有些耳机无法识别,看起来似乎无法工作。在互联网上搜索可能会找到解决方案。我在 Open SUSE 上遇到了这个问题,于是决定改用板载声卡。在 Windows 上运行 OpenAL 时我没有遇到这个问题。

以下是测试应用程序的屏幕截图和使用说明。此屏幕显示三个 Radio 控件、一个 Transmit 按钮和一个用于音频回发的复选框。底部的状态栏显示用于发送和接收的广播 IP 地址,以及接收到的数据包数量。

DIS Radio Transmitter Test Application

单击第一个 Radio 控件(显示为被浅灰色框包围的中灰色框)。单击后,将弹出以下对话框

Radio Interface Selection form

在此测试用例中,勾选 Transmit/Receive 复选框。这将使第一个 Radio Control 成为发射器和接收器。然后输入一个频率,在本例中请使用 1,输入到提供的框中。选择 Apply。完成后,主窗体将显示输入的频率。

DIS Radio Test Application Radio Transmitter Setup

此时,勾选 Loop Back Audio 复选框。这将允许任何传输的音频回放到扬声器。按下 TRANSMIT 按钮并对着麦克风说话,应该能从扬声器听到音频。

颜色指示器如下

  • BaseBackgroundShadowColor 设置为 Transparent,表示此 Radio Control 既不是发射器也不是接收器。
  • ReceiverColor 设置为 DarkBlue,表示 Radio Control 仅为接收器。
  • ReceivingSignalColor 设置为 Yellow,这是接收传输时显示的频率的字体颜色。
  • TransmitterColor 设置为 Red,表示 Radio Control 是发射器和接收器。
  • TransmittingColor 设置为 Maroon,这是 Transmitting 状态下 Radio Control 的背景颜色。

Radio Control Properties

注释

我的目标是提供一个关于如何使用 OpenAL API 实现语音播放的快速示例。OpenAL 还有许多其他功能未涵盖,但这个示例至少应该能让你入门。此外,此应用程序是使用 Visual Studio 2008 开发的,因此在 MonoDevelop 中没有 Windows Forms(也许不久的将来会有),因此使用该产品时应使用 GTK# Visual Designer。

在测试音频期间,我启用了 Push-To-Talk 以始终处于传输状态。几天后,我注意到在 Windows 系统上,音频缓冲区明显滞后,以至于当有人在 Linux 发射器上讲话时,要过几分钟才能在 Windows 机器上听到。在类似的应用程序中,我曾为仅限 Windows 开发过使用 WaveInOpen (Win32 Audio) 的程序,也存在这个问题。由于几乎不可能一直有一个活动的发射器,因此我预计不会出现问题,但作为一种修复,我创建了两个源并在这两个源之间切换传入的音频。这使得缓冲区有时间清除,并且由于传入数据是串行化的(由于套接字和传入数据锁定的性质),因此一个音频数据包在之前的音频数据包播放之前发生的几率应该不会发生。

由于上述 OpenAL 的问题,我从未能够在 Linux 设置上测试 USB 耳机。在我进行的研究搜索中有一些修复方案,但由于我不是 Linux 专家,我决定只使用板载声卡,效果很好。我只能用三个系统(两个 Windows 和一个 Suse Linux)进行测试,所以我不知道该软件的扩展性如何。

历史

  • 2010 年 2 月 28 日:初始发布
  • 2010 年 3 月 12 日:更新源代码
© . All rights reserved.