openal资料转贴
地址:http://blog.sina.com.cn/s/blog_685b5b220100ukbp.html
OpenAL简介
OpenAL(Open Audio
Library)是专门负责3D定位音效方面的API,可用来开放地、跨平台地访问声音硬件。与那些今日在游戏中得到普遍应用的较大的面向对象的库相比,OpenAL是一个简单明了的替代方案。OpenAL一直在不断的创新,几乎没有一个API能达到她的全部潜能。一个很大的原因是因为hardware加速建立在特殊的版卡上。然而,Creative
Labs是OpenAL的主要支持者,同时也是最大声卡厂商之一。OpenAL的另一个主要支持者是LOKI。
OpenAL不是商业产品,那样做限制了她的发展。OpenAL有许多的潜能,有许多的声音库工作在最底层的硬件上。但OpenAL的设计者经过无数的测试使她成为一个高级的API。她的风格是自由的,不同的编码风格和硬件部件将充分运用她的功能。有OpenGL编程精练的人将很快掌握OpenAL。
现在在PC竞技场中,游戏玩家实际上只有一种声音卡可以选择 -- PC声卡制造商创新公司(Creative Labs)的Sound Blaster Live!
从旧的时间个人计算机声音卡片制造业者有创造力的中心.
多年来创新公司已经为DirectX提供了他们的EAX声音扩展,并且他们是发起新的OpenAL(开放音频库Open Audio
Library)的创立者。就如同OpenGL是一个图形API一样,OpenAL,像它起来听一样,是一个声音系统的API。OpenAL
被设计为支持大多数通常声卡的许多特征,而且在一个特定的硬件特征不可得时提供一个软件替代。
OpenAL
被设计为支持大多数通常声卡的许多特征,而且在一个特定的硬件特征不可得时提供一个软件替代。
创新公司的Garin Hiebert给出了OpenAL定义:
"这里借用我们的 " OpenAL 规格和叁考" 的一个定义: OpenAL
是对音频硬件的一个软件接口,给程序员提供一个产生高质量多通道输出的能力。OpenAL
是在模拟的三维环境里产生声音的一种重要方法。它想要跨平台并容易使用,在风格和规范上与OpenGL相似。任何已经熟悉OpenGL的程序员将发现OpenAL非常熟悉。
OpenAL
API能容易地被扩展适应插件技术.创新公司已经把EAX支持加入到这套API了,程序员可以用来给他们的声音环境增加复杂的反响,比赛和障碍效果。
如同Jedi
Knight II: Outcast 一样,连同Eagle 世界/声音特征编辑器,Soldier of
Fortune II 以这个新系统为特征。什么是Eagle?
这是一个编辑器,允许多数第一人称射击游戏地图设计者将他们的地图导入到这个工具,然后构造简化的几何形体来为实际游戏引擎中的EAX代码产生一个声音地图。其思想是你不需要一个真实的图形地图的复杂几何形体来模拟声音环境。你也能够给产生的简化地图分配声音物质,这样声音环境就能够动态地改变。当你跳入水中时,听到所有的声音改变;当发声物体如飞机或火车在你身边呼啸而过时,你会感受到声音的多普勒效应,这是一个非常令人沉浸的经历。
另外的一个系统是Miles声音系统。Miles是一家公司,它为你的代码生产插件,在充分利用每块声卡时处理所有必须的到特定声音卡的说话(比如Sound
Blaster Live!系列,或者老的A3D声卡)。它非常像一个API前端,捆绑了一些额外的特征在里面。
在其他事物当中Miles让你存取一些事物像MP3解压缩。
它是很好的解决方案,但像任何事一样,它花费金钱并是你的代码和硬件之间的额外一层。虽然对於快速的声音系统制造,它非常有用,而且他们有段时间了,因此他们的确精通自己的业务。
OpenAL的主页为:http://www.openal.org。在这里可下载到OpenAL在各种操作系统上的实现及其文档。
OpenAL API
这部分将主要介绍OpenAL的接口,从基本的概念到可选的扩展都会做一些说明。
1、OpenAL入门
OpenAL从本质上讲是一个音频场景图库(audio scene graph
library)。它描述对象之间的一系列关系。大部分的对象体现了离散的概念。其中重要概念有设备(device)、渲染上下文环境的描述表(context)、听众(listener)、音源(source)、缓冲器(buffer)等。大部分的OpenAL条目都和这些类型的创建、销毁或者属性改变有关。
一般而言,对象之间有如下关系:设备是最终输出PCM(Pulse Code
Modulation,脉冲编码调制)数据的硬件。一个listener属于且仅属于一个context,而每个context也刚好只能有一个
listener。因此context就是在场景中聆听声音的对象实例。通常,每个场景中有一个listener,有对应的位置和其他应用程序用户属性。
缓冲器中存储的是原始PCM样本数据,不能直接播放。只有把缓冲器和音源关联起来,并播放该声音,声音才能被渲染出来。一个音源可以和多个缓冲器相关联,此时我们称其拥有一个缓冲器队列(Buffer
Queue)。
音源和缓冲器一般通过名字(name)来引用,名字是整形标识符(不同的对象类型具有惟一对应的名字)。例如,没有两个音源名字会相同,介但它们可能与某些缓冲器的数字ID重复。
对象初始化以及名字绑定的语法是alGen{Object}。相应地,销毁对象时调用alDelete{Object}。例如,分别调用函数alGenSource()和alDeleteSource()来创建和销毁音源对象。创建context和device的函数一此不同,稍后会详细讨论。
音源是和context相关的。在一个context内有效的音源名字在其他的context中无效。缓冲器是和context无关的,创建缓冲器无需引用任何当前活动的context。缓冲器能够同时在多个context中与多个音源关联。
这些对象是的大部分都具有一些可以直接设定和查询的属性(Attribute)。属性有一个特定的类型,也有默认值。最常用的是音源属性,通过音源属性可
以使缓冲器和一些音源相关联,还可以设置某一音源的位置等等。listener与音源在设定和查询属性方面具有相似的语法,都是al{Object}
{n}{if}{v}。大部分属性访问都是以数字(n)或者向量(v)形式来表示,而所传递或接受的参数的类型是这样指定的:i代表整数,f代表浮点数。
例如,音源的位置通过函数alSource3f()或alSourcefv()带上AL_POSITION标记来设置。
最重要的缓冲属性,即组成声音的PCM样本集,是通过函数alBufferData()来设定的。
下面是一小段OpenAL程序例子。
//打开设备,创建设备
ALCdevice *dev = alcOpenDevice(NULL);
ALCcontextCurrent *cc = alcCreateContext(dev, NULL);
alcMakeContextCurrent(cc);
//创建音源和缓冲器
ALuint bid, sid;
alGenSources(1, &sid);
alGenBuffers(1, &bid);
//取得pcm数据,用缓冲区来关联它
ALvoid *data;
ALsizei size, bits, freq;
ALenum format;
ALboolean loop;
alutLoadWAVFile("boom.wav",
&format, &data,
&size, &freq,
&loop);
alBufferData(bid, format, data, size, freq);
//用音源关联缓冲器
alSourcei(sid, AL_BUFFER, bid);
//播放音源然后等待直到完成
//然后释放它
alSourcePlay(sid);
ALint state;
do{
alGetSourcei(sid, AL_SOURCE_STATE, &state);
}while(state == AL_PLAYING);
alDeleteSources(1,
&sid);
alDeleteBuffers(1, &bid);
alcMakeContextCurrent(NULL);
alcDestroyContext(cc);
alcCloseDevice(dev);
如上述程序所示, 函数alcOpenDevice()用于打开设备,
它带有一个可选的设备指示字符串参数。该字符串参数的语法和含义是与实现相关的。这意味着允许应用程序指定另外的后端或与设备相关的配置参数。在
GNU/Linux的参考实现中,该设备字符串参数作为LISP语言风格的标记被解释,能够指定多个后端以及一些属性诸如采样率和后端相关功能。
函数alcCreateContext()用于创建渲染上下文环境,创建时需要指定一个设备用作context中混音的渲染目标。另外这个函数还可以琏可选的context属性表参数,形式是以零终止的整数对。需要由实现支持的context属性包括有ALC_SYNC、ALC_REFRESH及ALC_FREQUENCY。ALC_REFRESH和ALC_FREQUENCY会影响context渲染的性能与保真度,
而ALC_SYNC令context仅在调用函数alcProcessContext()进行更新后才会进行混音。
如前例所示的那样,OpenAL在语法、编码风格和习惯上都有模仿OpenGL。做出这个决定是为了在一定程度上迎合那些已经熟悉OpenGL的开发人员,也是为了仿效OpenAL
ARB所代表的切合实际的设计原则。
2、进阶
可通过alSource{n}{if}{v}条目来设定音源的属性。音源属性可以公成三组:第一组影响音源在OpenAL世界中的物理位置,例如
AL_POSITION和AL_VELOCITY;第二组表示“旋钮和转盘”如何影响音源的高级别的管理很有用处的状态属性,例如AL_LOOPING和
AL_SOURCE_STATE。
使用AL_POSITION属性来设置音源位置是世界坐标系中的位置。只有带了附加属性AL_SOURCE_RELATIVE的时候才例外,这个属性告诉
实现程序以渲染上下文环境的听从作为它原点来定位。这个属性在像类似可能从头盔发出头部相关的声音或者像音乐这种“2D”声音很有用处。对于那些无需定位
的声音,常常使用internalFormat(多通道)扩展来实现,这会在后面进行介绍。
音源的AL_PITCH属性用于控制某一声音的相对音高。取值为1.0的时候,渲染的音源上无需调高。每减少50%会导致一个八度(-12半音)的音高变
化。在GNU/Linux实现下,多普勒频率滤波器计算多普勒效应作为现有的音高属性的放缩因子。在应用程序中生动地使用可以达到非常好的效果。而通过软
件实现音源的音高变化的代价是昂贵的,因此使用前合理地判断是必要的。
多普勒效应说明了OpenAL API的一些亮点。假如想使用多普勒效应,则必须设定listener和音源的一些属性。
ALfloat l_pos[] = {0,0,5};
ALfloat s_pos[] = {0,0,5}, s_vel[] = {0,0,1};
ALfloat zeros[] = {0,0,0};
alListenerfv(AL_POSITION,
l_pos);
alListenerfv(AL_VELOCITY, zeros);
alSourcefv(sid, AL_POSITION,
s_pos);
alSourcefv(sid, AL_VELOCITY, s_vel);
alSourcePlay(sid);
ALint state;
do{
s_vel[2] += 0.001;
s_pos[2] += 0.001;
alSourcefv(sid, AL_VELOCITY, s_vel);
alSourcefv(sid, AL_POSITION, s_pos);
alGetSourcei(sid,
AL_SOURCE_STATE, &state);
}while(state != AL_PLAYING);
本例是需要注意的是,音源位置的计算不是推导出来的——而是由应用程序明确设置的。同时假定所有的位置和速度都是即时的。
OpenAL通过缓冲器排队机制支持声音的流式播放。缓冲器排队是多个缓冲器与单一音源相关联的一种机制。当音源播放时,连续对各个缓冲器进行渲染,就好象这些缓冲器组成了一个连续的声音。这可以通过一些特殊函数来控制。
流音源的工作一般是这样的。音源里的一批缓冲器通过alSourceQueueBuffers()函数进行排队,然后播放音源,接下来用属性
AL_BUFFERS_PROCESSED来查询。该属性得出已经处理好的缓冲器的数量,从而允许应用程序使用
alSourceUnqueueBuffers()函数删除那些已经处理好的缓冲器。alSourceUnqueueBuffers()函数将从队列头部
开始依次将处理好的缓冲器删除。最后,其余的缓冲器在音源上排队。当缓冲器正在播放时,试图移去缓冲器会得到一个错误。
//使用排队机制来关联到缓冲器第一个集
alSourceQueueBuffers(sid, NUMBUFFERS, Buffers);
alSourcePlay(sid);
ALuint count = 0;
ALuint buffers_returned = 0;
ALint processed = 0;
ALboolean bFinished = AL_FALSE;
ALuint buffers_in_queue = NUMBUFFERS;
while(!bFinished)
{
//取得状态
alGetSourceiv(sid, AL_BUFFER_PROCESSED,
&processed);
//假如播放完毕了一些缓冲器,然后让它们退出队列
//然后装载新的音频,再把它们装入队列
if(processed>0)
{
buffers_returned += processed;
while(processed)
{
ALuint bid;
alSourceUnqueueBuffers(sid, 1, &bid);
if(!bFinished)
{
DataToRead = (DataSize>BSIZE) ? BSIZE :
DataSize;
if(DataToRead == DataSize)
bFinish = AL_TRUE;
//.......
//省略从音源读出DataToRead字节的代码
DataSize -= DataToRead;
if(bFinish == AL_TRUE)
memset(data + DataToRead, 0, BSIZE - DataToRead);
alBufferData(bid, format, data, DataToRead,
wave.SamplesPerSec);
//对缓冲器排队
alSourceQueueBuffers(sid, 1, &bid);
processed--;
}
else
{
processed--;
if(buffers_in_queue-- == 0)
{
bFinished = AL_TRUE;
break;
}
}
}
}
}
3、空间定位
OpenAL的核心是将声音的衰减表现为某一距离函数。OpenAL有一系列的距离模型可以在运行的时候选择。
函数alDistaneceModel()用于在不同的距离模型中进行了选择。默认的距离模型是AL_INVERSE_DISTANCE,遵守下面的公式:
G_db=clamp(GAIN-20*log10(1+Rf*(dist-Rd)/Rd, MinG, MaxG))
此公式中Rf和Ed对应于音源的两个属性:AL_ROLLOFF_FACTOR和AL_RDFERENCE_DISTANCE。
MinG和MaxG分别对应于音源的最小增益属性AL_MIN_GAIN和最大增
益属性AL_MAX_GAIN。参考距离dist是listen体验增益(GAIN)的距离。依音源而定的rolloff系数(高低频规律性衰减系数)能
够在值变化量的负方向上改变音源的范围。当rolloff系数为0表明对于音源没有衰减。
4、扩展与alut库
OpenAL具有和OpenGL相似的可扩展性。应用程序首先调用函数alGetString(AL_EXTENSIONS)来询问实现。此函数返回一个
可在其中搜索特定标识的扩展字符串。此外,函数alIsExtensionPresent()可以确定是否存在某个扩展。一旦确定某一扩展的存在,就用程
序将能够通过函数alGetProcAddress()和alGetEnumValue()取得特定的函数和枚举标记。
OpenAL核心库中没有用于处理文件格式的函数。该功能由alut辅助库提供实现。函数alutLoadWavFile()和
alutLoadWavMemory()可以加载不同版本的WAV文件格式。在提供便于应用程序载入音频文件函数的同时,alut还有简化初始化和结束程
序的alutInit和alutExit例程。它们隐藏了context和设备的初始化细节,不过要稍稍损失一些灵活性作为代价。
#include
<conio.h>
#include <time.h>
#include <stdlib.h>
#include <al/al.h>
#include <al/alc.h>
#include <al/alu.h>
#include <al/alut.c>
// 我们需要的最大的数据缓冲.
#define NUM_BUFFERS 3
// 我们需要放三种声音.
#define NUM_SOURCES 3
// 缓冲和源标志.
#define BATTLE 0
#define GUN1 1
#define GUN2 2
// 存储声音数据.
ALuint Buffers[NUM_BUFFERS];
// 用于播放声音.
ALuint Sources[NUM_SOURCES];
// 源声音的位置.
ALfloat SourcesPos[NUM_SOURCES][3];
// 源声音的速度.
ALfloat SourcesVel[NUM_SOURCES][3];
// 听者的位置.
ALfloat ListenerPos[] = { 0.0, 0.0, 0.0 };
// 听者的速度.
ALfloat ListenerVel[] = { 0.0, 0.0, 0.0 };
// 听者的方向 (first 3 elements are
"at", second 3 are "up")
ALfloat ListenerOri[] = { 0.0, 0.0, -1.0, 0.0, 1.0, 0.0 };
ALboolean LoadALData()
{
// 载入变量.
ALenum format;
ALsizei size;
ALvoid* data;
ALsizei freq;
ALboolean loop;
// 载入WAV数据.
alGenBuffers(NUM_BUFFERS,
Buffers);
if (alGetError() !=
AL_NO_ERROR)
return AL_FALSE;
alutLoadWAVFile("wavdata/Battle.wav", &format,
&data, &size,
&freq, &loop);
alBufferData(Buffers[BATTLE], format, data, size, freq);
alutUnloadWAV(format, data, size, freq);
alutLoadWAVFile("wavdata/Gun1.wav", &format,
&data, &size,
&freq, &loop);
alBufferData(Buffers[GUN1], format, data, size, freq);
alutUnloadWAV(format, data, size, freq);
alutLoadWAVFile("wavdata/Gun2.wav", &format,
&data, &size,
&freq, &loop);
alBufferData(Buffers[GUN2], format, data, size, freq);
alutUnloadWAV(format, data, size, freq);
// 捆绑源.
alGenSources(NUM_SOURCES,
Sources);
if (alGetError() !=
AL_NO_ERROR)
return AL_FALSE;
alSourcei (Sources[BATTLE],
AL_BUFFER, Buffers[BATTLE] );
alSourcef (Sources[BATTLE], AL_PITCH, 1.0 );
alSourcef (Sources[BATTLE], AL_GAIN, 1.0 );
alSourcefv(Sources[BATTLE], AL_POSITION, SourcePos[BATTLE]);
alSourcefv(Sources[BATTLE], AL_VELOCITY, SourceVel[BATTLE]);
alSourcei (Sources[BATTLE], AL_LOOPING, AL_TRUE );
alSourcei (Sources[GUN1],
AL_BUFFER, Buffers[GUN1] );
alSourcef (Sources[GUN1], AL_PITCH, 1.0 );
alSourcef (Sources[GUN1], AL_GAIN, 1.0 );
alSourcefv(Sources[GUN1], AL_POSITION, SourcePos[GUN1]);
alSourcefv(Sources[GUN1], AL_VELOCITY, SourceVel[GUN1]);
alSourcei (Sources[GUN1], AL_LOOPING, AL_FALSE );
alSourcei (Sources[GUN2],
AL_BUFFER, Buffers[GUN2] );
alSourcef (Sources[GUN2], AL_PITCH, 1.0 );
alSourcef (Sources[GUN2], AL_GAIN, 1.0 );
alSourcefv(Sources[GUN2], AL_POSITION, SourcePos[GUN2]);
alSourcefv(Sources[GUN2], AL_VELOCITY, SourceVel[GUN2]);
alSourcei (Sources[GUN2], AL_LOOPING, AL_FALSE );
// 做错误检测并返回
if( alGetError() !=
AL_NO_ERROR)
return AL_FALSE;
return AL_TRUE;
}
首先,我们导入文件数据到3个缓冲区,然后把3个缓冲区和3个源锁在
一起。唯一的不同是文件“battle.wav”在不停止时循环。
void SetListenervalues()
{
alListenerfv(AL_POSITION, ListenerPos);
alListenerfv(AL_VELOCITY, ListenerVel);
alListenerfv(AL_ORIENTATION, ListenerOri);
}
void KillALData()
{
alDeleteBuffers(NUM_BUFFERS, &Buffers[0]);
alDeleteSources(NUM_SOURCES, &Sources[0]);
alutExit();
}
int main(int argc, char
*argv[])
{
// Initialize OpenAL and clear the error bit.
alutInit(NULL, 0);
alGetError();
// Load the wav data.
if (LoadALData() == AL_FALSE)
return 0;
SetListenervalues();
// Setup an exit procedure.
atexit(KillALData);
// Begin the battle sample to
play.
alSourcePlay(Sources[BATTLE]);
// Go through all the sources and
check that they are playing.
// Skip the first source because it is looping anyway (will always
be playing).
ALint play;
while (!kbhit())
{
for (int i = 1; i < NUM_SOURCES; i++)
{
alGetSourcei(Sources[i], AL_SOURCE_STATE,
&play);
if (play != AL_PLAYING)
{
// Pick a random position around the listener to play the
source.
double theta = (double) (rand() %
360) * 3.14 / 180.0;
SourcePos[i][0] =
-float(cos(theta));
SourcePos[i][1] = -float(rand()%2);
SourcePos[i][2] = -float(sin(theta));
alSourcefv(Sources[i],
AL_POSITION, SourcePos[i] );
alSourcePlay(Sourcev[i]);
}
}
}
return 0;
}
openal资料转贴的更多相关文章
- 【原】iOS学习45之多媒体操作
1. 音频 1> 音频实现简述 iOS 里面共有四种专门实现播放音频的方式: System Sound Services(系统声音服务) OpenAL(跨平台的开源的音频处理接口) Audio ...
- openal在vs2010中的配置
下载openal开发工具:相关资料可以在OpenAL官网http://connect.creativelabs.com/openal/default.aspx上获得.这里下载的SDK为OpenAL11 ...
- OpenCL、OpenGL、OpenAL
一:OpenCL (全称Open Computing Language,开放运算语言)是第一个面向异构系统通用目的并行编程的开放式.免费标准,也是一个统一的编程环境,便于软件开发人员为高性能计算服务器 ...
- 使用openal与mpg123播放MP3,附带工程文件(转)
使用openal与mpg123播放MP3,附带工程文件 使用openal和mpg123播放MP3文件 使用静态编译,相关文件都在附件里 相关工程文件:openal_mpg123_player.7z 使 ...
- OpenAL介绍
OpenAL(Open Audio Library)是自由软件界的跨平台音效API,由Loki Software,使用在Windows.Linux 系统上,用在音效缓冲和收听中编码. OpenAL设计 ...
- Vim新手入门资料和一些Vim实用小技巧
一些网络上质量较高的Vim资料 从我07年接触Vim以来,已经过去了8个年头,期间看过很多的Vim文章,我自己觉得非常不错,而且创作时间也比较近的文章有如下这些. Vim入门 目前为阿里巴巴高级技术专 ...
- Git入门资料汇总
Git是一个非常好用的版本控制工具,同时,它也是一个相对比较复杂的工具,想要掌握它还是需要花一番功夫的.网络上关于Git的入门资料已经很多了,我就不再重复了,直接把我学习的文章放在这里. Git详解 ...
- MVC5 网站开发之七 用户功能 3用户资料的修改和删除
这次主要实现管理后台界面用户资料的修改和删除,修改用户资料和角色是经常用到的功能,但删除用户的情况比较少,为了功能的完整性还是坐上了.主要用到两个action "Modify"和& ...
- webapi的学习资料
猿教程_-webapi教程-WebAPI教程 猿教程_-webapi教程-Web API概述 猿教程_-webapi教程-新建Web Api项目 猿教程_-webapi教程-测试Web API 猿教程 ...
随机推荐
- java Web服务实现方案(REST+SOAP+XML-RPC)简述及比较
目前知道的三种主流的Web服务实现方案为:REST:表象化状态转变 (软件架构风格)SOAP:简单对象访问协议 XML-RPC:远程过程调用协议 下面分别作简单介绍: REST:表征状态转移(Repr ...
- 如何在 Visual Studio 2013 中调试.NET Framework 4.5.1 代码
版本需求如标题,在 工具->选项->调试->常规 中,更改以下设置: 禁用:启用“仅我的代码”.逐过程执行属性和运算符(仅限托管).要求源文件与原始版本完全匹配 启用:启用 .NET ...
- 【Android】7.8 MyDemos项目的结构和主界面相关代码
分类:C#.Android.VS2015: 创建日期:2016-02-17 一.简介 上一讲已经说过,系统升级为Win10后,重新创建了一个新的项目:MyDemos,并把前7章合并到了这个项目中,这次 ...
- 每日英语:China Overtakes U.S. in Number of Diabetes Cases
China is now home to the world's largest diabetes population. The number of people who have diabetes ...
- css 点点加载demo
content字符生成配合CSS3 animation的点点点loading效果实例 代码: CSS代码: dot { display: inline-block; height: 1em; line ...
- XPath学习:轴(1)——child
http://www.cnblogs.com/zhaozhan/archive/2009/09/10/1563723.html ************************************ ...
- [转]采购文件中 RFI、RFQ、RFP、IFB的区别
原文:http://www.douban.com/group/topic/23426427/ [PMBOK的描述] 采购文件用于征求潜在卖方的建议书.如果主要依据价格来选择卖方(如购买商业或标准产品 ...
- mysql show variables
1. back_log 指定MySQL可能的连接数量.当MySQL主线程在很短的时间内得到非常多的连接请求,该参数就起作用,之后主线程花些时间(尽管很短)检查连接并且启动一个新线程. back_log ...
- 一款基于jquery实现的鼠标单击出现水波特效
今天要为大家绍一款由jquery实现的鼠标单击出现水波特效.用鼠标猛点击页面,你可以看到页面不断出面水波纹效果.然后水波纹渐渐消失.效果非常不错.我们一起看下效果图: 在线预览 源码下载 实现的代 ...
- 如何测试Linux 中的wait函数能不能等待子进程的子进程?
#include <stdio.h> #include <stdlib.h> int main() { pid_t pid = fork(); switch(pid) { : ...