大约在 50 年前,一名物理学家和工程师名叫罗伯特 · 穆格电子音乐合成器功能创建的颇不寻常:器官型键盘。 一些电子音乐的作曲家轻视这种平淡和老式的控制设备,同时其他作曲家 — — 和特别是表演者 — — 对此发展表示欢迎。 1960 年代末,由温卡洛斯的 Switched-On 巴赫已成为最畅销的古典专辑的所有时间,和 Moog 合成器进入了主流。
早期的 Moog 合成器是模块化的编程与跳线。 1970 年,然而,Minimoog 被释放 — — 小、 易于使用和发挥,和售价仅为 1,495 元。 (这些早期合成器好历史是一本书,"模拟天:发明和影响 Moog 合成器"[哈佛大学出版社,2004 年],由 Trevor 捏和弗兰克 Trocco.)
我们将分类 Moog 和类似合成器作为"模拟"设备因为他们创建使用不同电压从修造从晶体管、 电阻、 电容电路生成的声音。 相比之下,更现代的"数码"合成器创建声音通过算法计算或数字化的样品。 较旧的设备进一步被列为"减法"合成器:而不是建筑通过结合正弦波 (称为添加剂的合成技术) 的复合声,消减合成器开始与丰富的谐波的波形 — — 如锯齿或方形波 — — 然后运行它通过筛选器,以消除某些谐波和改变声音的音色。
由罗伯特 · 穆格率先的关键概念是"电压控制"。考虑振荡器,它是生成的某种基本的音频波形合成器的组件。在早些时候的合成器,可能由一个电阻的电路,在某个地方的值控制这个波形的频率和可能通过拨号控制的可变电阻器。 但在压控振荡器 (VCO),振荡器的频率受输入电压。 例如,每增加一个伏到振荡器可能双振荡器的频率。 这种方式,可以通过键盘生成增加每倍频程一伏特的电压控制压控振荡器的频率。
在模拟合成器,从一个或多个 Vco 输出进入压控滤波器 (VCF) 改变波形的谐波含量。 输入的电压到 VCF 控制滤波器的截止频率或筛选器的反应 (筛选器的质量或 Q) 的清晰度。 VCF 的输出然后进入压控的放大器 (VCA),其中的增益由另一个电压控制。
信封发电机
但是,一旦你开始谈论勾通和 VCAs,事情就变得复杂,和一个小背景是必要的。
早在 19 世纪,一些科学家 (最突出的是 Hermann von Helmholtz) 开始进军重大,物理学的探索和声音的感觉。 声音频率和响度变得相对简单等特点与疑难问题的音色相比 — — 这使我们能够分辨钢琴小提琴或伸缩喇叭的声音的质量。 它是假设 (和有些证明) 音色与声音的谐波含量,是不同程度的正弦曲线,构成了声音的强度有关。
但 20 世纪研究人员开始时作进一步的调查,他们发现不是这么简单。 谐波含量变化对音乐的音调,当然,这有助于乐器的音色。 特别是注意从乐器的一开始对听觉感知至关重要。 在钢琴的锤子或小提琴弓首先触及的字符串,或振动空气推进入金属或木制的管时,会发生非常复杂的谐波活动。 这种复杂性降低速度非常快,但如果没有它,音乐声调声音沉闷和远不如有趣和独特。
模仿真实音乐声调的复杂性,合成器只是不能打开注意打开和关闭开关一样。 (以这种简单的合成器听一样,检查出 2013 年 2 月安装在此列中的 ChromaticButtonKeyboard 程序msdn.microsoft.com/magazine/jj891059.)在每个注释的开始,声音稳定之前,必须有简介"昙花一现"的高容量和不同的音色。 注意结束时,声音必须不只是停止,但减少的数量和复杂性与消亡。
响度,那里是对这一进程的一般模式:演奏的字符串、 黄铜或木管乐器的仪器上的说明,声音迅速上升到最大音量,然后死一点并保持稳定。 注意结束时,它迅速地减少卷中。 这两个阶段被称为的"攻击"和"释放"。
为更多的撞击式打桩设备 — — 包括钢琴 — — 说明在攻击期间迅速达到最大音量,但然后死慢慢如果仪器仍未受潮,例如,在钢琴键按住。 一旦释放该键,注很快死。
要实现这些效果,合成器实现叫做"信封生成器"。图 1 显示调用攻击-朽烂-持续释放 (ADSR) 信封相当标准的例子。 横轴是时间,与垂直轴是响度。
图 1 攻击衰变持续释放信封
当按下键盘上的键注释开始对声音、 你听到给水管爆裂的声音首先,攻击和朽烂的部分,然后注意在承受水平稳定下来。 当释放键和注意两端,释放部分发生。 为钢琴类型的声音,衰变时间可能是几秒钟,并承受水平订为零,所以继续衰变,只要按住键的声音。
即使最简单的模拟合成器有两个 ADSR 信封:其中一个控制音量和其他控件的筛选器。 这通常是一个低通滤波器。 注意被触击,截止频率较快的增长,以允许通过,更多的高频谐波,然后截止频率稍有下降。 这强调了很多,创建独特的模拟合成器鸣声。
AnalogSynth 项目
约九个月前,当我正在考虑使用 XAudio2 小 1970年年代模拟合成器的数字仿真的程序,我意识到信封发电机会的工作更具挑战性的方面之一。 它不是即使我清楚是否这些信封发电机外部音频处理流 (并因此 SetVolume 和 SetFilterParameters 的访问方法 XAudio2 语音),或以某种方式被内置到音频流。
我最终定居在执行作为 XAudio2 音频效果的信封上 — — 更正式称为音频处理对象 (APOs)。 这意味着信封的逻辑上的音频流直接工作。 编码重复数字的二阶滤波器的滤波逻辑内置于 XAudio2 之后,我变得更有信心,这种方法。 通过使用我自己的筛选器代码,我以为也许能够更改程序的结构的滤波算法在将来受到重大干扰。
图 2 显示的屏幕,由此产生的模拟的Synth 程序,其源代码的代码你可以在下载archive.msdn.microsoft.com/mag201307DXF。 虽然我受 Minimoog 上控件的布局,我保持实际 UI 相当简单,例如,使用滑块而不拨。 我大部分是重点的在内部结构上。
图 2 AnalogSynth 屏幕
键盘是一系列自定义键控件处理指针事件并分组到八度的控件。 键盘是实际上六个八度音阶的宽度,可以使用下面的键厚厚的灰色条纹水平滚动。 红点标识中间 C.
程序可以发挥 10 同时注意到,但那是用一个简单的 #define MainPage.xaml.cs 在多变。 (Minimoog 像早期的模拟合成器是单声道)。每一个这些 10 声音是实例类的我叫 SynthVoice。 SynthVoice 已设置声音 (包括频率、 数量和信封) 的所有各项参数的方法,以及命名触发器和释放来指示当键已按下或释放的方法。
Minimoog 实现其特征的"强力"声音部分由两个并行运行的振荡器,往往稍有 mistuned,故意或频率漂移在模拟电路中常见。
因此,每个 SynthVoice 创建两个类的实例振荡器,它从左上角的控制面板中所示控制图 2。 控制面板允许您设置的波形和相对体积这些两个振荡器,并向上或向下,可以由一个或两个八度音阶换位频率。 此外,您可以抵消的第二个振荡器的频率达八度。
振荡器的每个实例创建一个 IXAudio2SourceVoice 对象,并公开命名为 SetFrequency、 SetAmplitude 和 SetWaveform 的方法。 SynthVoice 将两个 IXAudio2SourceVoice 输出路由到 IXAudio2SubmixVoice,然后实例化两个自定义的音频效果,称为 FilterEnvelopeEffect 和振幅EnvelopeEffect,它适用于此 submix 声音。 这两个效果分享叫 EnvelopeGenerator,我不久就会描述一类。
图 3 每个 SynthVoice 中显示组件的组织。 10 SynthVoice 对象,总共有 20 IXAudio2Source语音进入 10 个 IXAudio2SubmixVoice 实例,然后路由到单一的 IXAudio2MasteringVoice 的实例。 我使用 48,000 Hz 和整个 32 位浮点样品的采样率。
图 3 SynthVoice Class 的结构
用户控制中心部分的控制面板中的筛选器。 切换按钮允许筛选器来绕过 ; 否则,截止频率是与正在播放的注意。 (换句话说,滤波器的截止频率跟踪键盘)Emphasis 滑块控制筛选器的 Q 设置。 信封滑块控制信封对滤波器截止频率的影响的程度。
过滤器封套和响度信封与关联的四个滑块同样的工作。 攻击、 衰变和释放的滑块是所有持续时间从 10 毫秒到 10 秒的对数刻度。 滑块有工具提示值转换器,以显示与设置相关联的持续时间。
AnalogSynth 使潜在的同时 IXAudio2SourceVoice 实例 20 或抵制的趋势数字二阶滤波器的截止频率附近的音频放大无音量调整。 因此,AnalogSynth 容易过载,音频。 为了帮助避免这种情况的用户,该程序使用 XAudio2CreateVolumeMeter 函数来创建监视传出声音的音频效果。 如果在右上角的绿点更改为红色,音频输出被剪切,你应该使用最右边来减小音量滑块。
早期合成器插接线用于连接组件。 由于这一遗产,特别是合成器安装程序仍称为"补丁"。如果你找到了一个修补程序,您想要保留的声音,请按保存按钮,并指定一个名称。 新闻载入按钮来获取列表以前保存的修补程序,然后选择一个。 这些修补程序 (以及当前的设置) 都存储在本地设置区域中。
信封生成器算法
实现包络发生器的代码基本上是一个状态机,五个连续国家称休眠、 攻击、 衰减、 持续释放。 从 UI 的角度,看起来最自然的指定攻击,朽烂,和维持的时间 dura讨论,但当实际执行您需要的转换率的计算 — — 的增加或减少每单位时间的响度 (或滤波器截止频率)。 在 AnalogSynth 中的两个音频效果使用这些不断变化的水平来实现效果。
这个状态机并不总是作为顺序中的关系图作为图 1 似乎暗示。 例如,当一个键是按下和释放如此迅速信封尚未达到承受部分释放键时? 起初我以为信封应获准完成其攻击和朽烂的部分,然后走进释放部分中,但这并没有工作好钢琴式信封。 在钢琴的信封,承受水平是零和衰变时间相对较长。 快速按下并释放一个键仍有很长的朽烂 — — 如果它不在所有发布 !
我决定为的快速按下并释放,我会让攻击部分完成,但然后立即跳转到释放部分。 这意味着最后率的下降将需要计算基于目前的水平上。 这就解释了为什么有释放的信封参数结构中如何处理有差异,此处所示:
struct EnvelopeGeneratorParameters { float baseLevel; float attackRate; // in level/msec float peakLevel; float decayRate; // in level/msec float sustainLevel; float releaseTime; // in msec };
振幅包络,增员被设置为 0、 peakLevel 设置为 1 和 sustainLevel 是那些值之间的某个地方。 三级过滤器封套,提到了适用于滤波器截止频率的乘数:增员是 1,和 peakLevel 由标有"信封"滑块和范围可以从 1 到 16。 那倍频器的 16 对应于四个八度。
AmplitudeEnvelopeEffect 和 FilterEnvelopeEffect 共享的 EnvelopeGenerator 类。 图 4 显示 EnvelopeGenerator 的头文件。 请注意要设置信封的参数的公共方法和两个公共方法命名为攻击和释放触发信封开始和结束。 应按顺序调用这三个方法。 不编写代码来处理其参数通过其进展的中途转车的信封。
图 4 EnvelopeGenerator 头文件
class EnvelopeGenerator { private: enum class State { Dormant, Attack, Decay, Sustain, Release }; EnvelopeGeneratorParameters params; float level; State state; bool isReleased; float releaseRate; public: EnvelopeGenerator(); void SetParameters(const EnvelopeGeneratorParameters params); void Attack(); void Release(); bool GetNextValue(float interval, float& value); };
从信封发电机电流计算的值是通过重复调用 GetNextValue 获得的。 间隔 argu发言是以毫秒为单位,以及该方法计算新的值,基于该间隔可能切换过程中的国家。 信封已完成与释放部分,GetNextValue 返回为真时显示信封已完成,但我不实际使用该返回值别处的程序中。
图 5 显示的 EnvelopeGenerator 类的实现。 靠近顶部的 GetNextValue 方法是代码时释放率的计算基于当前级别和释放时间和释放键,跳过直接到释放状态。
图 5 执行 EnvelopeGenerator
EnvelopeGenerator::EnvelopeGenerator() : state(State::Dormant) { params.baseLevel = 0; } void EnvelopeGenerator::SetParameters(const EnvelopeGeneratorParameters params) { this->params = params; } void EnvelopeGenerator::Attack() { state = State::Attack; level = params.baseLevel; isReleased = false; } void EnvelopeGenerator::Release() { isReleased = true; } bool EnvelopeGenerator::GetNextValue(float interval, float& value) { bool completed = false; // If note is released, go directly to Release state, // except if still attacking if (isReleased && (state == State::Decay || state == State::Sustain)) { state = State::Release; releaseRate = (params.baseLevel - level) / params.releaseTime; } switch (state) { case State::Dormant: level = params.baseLevel; completed = true; break; case State::Attack: level += interval * params.attackRate; if ((params.attackRate > 0 && level >= params.peakLevel) || (params.attackRate < 0 && level <= params.peakLevel)) { level = params.peakLevel; state = State::Decay; } break; case State::Decay: level += interval * params.decayRate; if ((params.decayRate > 0 && level >= params.sustainLevel) || (params.decayRate < 0 && level <= params.sustainLevel)) { level = params.sustainLevel; state = State::Sustain; } break; case State::Sustain: break; case State::Release: level += interval * releaseRate; if ((releaseRate > 0 && level >= params.baseLevel) || (releaseRate < 0 && level <= params.baseLevel)) { level = params.baseLevel; state = State::Dormant; completed = true; } } value = level; return completed; }
一双音频效果
AmplitudeEnvelopeEffect 和 FilterEnvelopeEffect 的类从 CXAPOParametersBase 派生,所以他们可以接受参数,和两个类都还保持执行信封计算 EnvelopeGenerator 类的一个实例。 这些两个音频效果的参数结构命名 AmplitudeEnvelopeParameters 和 FilterEnvelopeParameters。
AmplitudeEnvelopeParameters 结构是仅仅是一个 EnvelopeGeneratorParameters 结构和布尔 keyPressed 字段,当这声音与相关联的键是按下和虚假当它被释放时是如此。 (筛选器EnvelopeParameters 结构是只是稍微更复杂,因为它需要纳入基地级滤波器截止频率和 Q 设置.)这两个效果类维护他们自己可以相比的参数值,以确定当信封攻击或释放的 keyPressed 数据成员状态应触发。
你可以看看这是如何工作图 6,其中 AmplitudeEnvelopeEffect 在演示的过程重写的代码。 如果已启用的效果和当地的 keyPressed 值为 false,但 keyPressed 效果的参数值为 true,此效果可使对 SetParameters 和攻击 EnvelopeGenerator 实例的方法的调用。 如果相反的情况 — — 本地 keyPressed 的值为 true,但在参数中的那个是假的 — — 然后影响调用 Release 方法。
图 6 在 AmplitudeEnvelopeEffect 的过程重写
void AmplitudeEnvelopeEffect::Process(UINT32 inpParamCount, const XAPO_PROCESS_BUFFER_PARAMETERS *pInpParam, UINT32 outParamCount, XAPO_PROCESS_BUFFER_PARAMETERS *pOutParam, BOOL isEnabled) { // Get effect parameters AmplitudeEnvelopeParameters * pParams = reinterpret_cast<AmplitudeEnvelopeParameters *> (CXAPOParametersBase::BeginProcess()); // Get buffer pointers and other information const float * pSrc = static_cast<float const*>(pInpParam[0].pBuffer); float * pDst = static_cast<float *>(pOutParam[0].pBuffer); int frameCount = pInpParam[0].ValidFrameCount; int numChannels = waveFormat.nChannels; switch(pInpParam[0].BufferFlags) { case XAPO_BUFFER_VALID: if (!isEnabled) { for (int frame = 0; frame < frameCount; frame++) { for (int channel = 0; channel < numChannels; channel++) { int index = numChannels * frame + channel; pDst[index] = pSrc[index]; } } } else { // Key being pressed if (!this->keyPressed && pParams->keyPressed) { this->keyPressed = true; this->envelopeGenerator.SetParameters(pParams->envelopeParams); this->envelopeGenerator.Attack(); } // Key being released else if (this->keyPressed && !pParams->keyPressed) { this->keyPressed = false; this->envelopeGenerator.Release(); } // Calculate interval in msec float interval = 1000.0f / waveFormat.nSamplesPerSec; for (int frame = 0; frame < frameCount; frame++) { float volume; envelopeGenerator.GetNextValue(interval, volume); for (int channel = 0; channel < numChannels; channel++) { int index = numChannels * frame + channel; pDst[index] = volume * pSrc[index]; } } } break; case XAPO_BUFFER_SILENT: break; } // Set output parameters pOutParam[0].ValidFrameCount = pInpParam[0].ValidFrameCount; pOutParam[0].BufferFlags = pInpParam[0].BufferFlags; CXAPOParametersBase::EndProcess(); }
效果可以打电话 EnvelopeGenerator 的 GetNextValue 方法为每个进程调用 (在这种情况下的时间间隔参数将指示 10 毫秒为单位) 或 (在这种情况下间隔是更像 21 微秒) 每个样本 虽然第一种方法应该是足够的我决定在理论上更平滑过渡为二。
将浮点卷返回值从 GetNextValue 呼叫范围从 0 (当注是第一次开始或结束) 为 1 的进攻高潮。 效果简单相乘的浮点样本通过此号码。
现在开始的乐趣
我花了那么多时间编码模拟Synth 程序,我没有很多时间来玩玩。 它很可能是一些控件和参数需要一些微调,或者也许相当粗糙的优化 ! 特别是,长时间衰变和释放卷上的时间听起来不是很对,他们建议振幅包络变化应该是对数的而不是线性。
此外令我大惑不解触摸输入与使用屏幕键盘。 真正的钢琴上的键是敏感的他们被触击,和合成器键盘有试图效仿这种相同的感觉的速度。 然而,大多数的触摸屏,不能检测触摸速度或压力。 但他们可作出轻微的手指运动对敏感的屏幕上,这是超出能力的一个真正的键盘。 屏幕键盘可更好地满足以这种方式吗? 那里是只有一种方法来找出 !
Charles Petzold 是 MSDN 杂志的长期撰稿人,他是“Programming Windows, 6th edition”(O'Reilly Media,2012)一书的作者,这本书讲授如何编写 Windows 8 应用程序。他的网站是charlespetzold.com。
衷心感谢以下技术专家对本文的审阅:詹姆斯 McNellis (Microsoft)
詹姆斯 McNellis 是一个 c + + 爱好者和微软的 Visual c + + 团队的软件开发人员在他那里他生成 c + + 库和维护的 C 运行时库 (CRT)。他在微博 @JamesMcNellis,并通过在线其他地方可以发现http://jamesmcnellis.com/。