我一直在制造电子乐器作为一种爱好现在大约
35 年。 我开始在晚 20 世纪 70 年代布线了 TTL 和 CMOS 芯片,于是晚得多的软件路由 — — 第一次与多媒体扩展到 1991 年的 Windows 和 Windows 演示文稿基础
(WPF) 中,和在 Silverlight 和 Windows Phone 7 的 MediaStreamSource 类 NAudio 图书馆最近。 就在去年,我专门讨论我触摸的一对夫妇分期付款
& 列去为 Windows Phone 应用程序播放声音和音乐。
我也许应该厌倦了这个时候,,或许不愿意探索又一次的声音代 API。 但我没有,因为我认为
Windows 8 可能是的最佳 Windows 平台尚未制作乐器。 Windows 8 将高性能的音频 API 结合在一起 — — XAudio2 的 DirectX 组件 — — 与上手持平板电脑触摸屏。这种结合提供了很大的潜力,而且我特别感兴趣探索如何可以作为一个微妙和亲密到乐器完全在软件中实现接口利用触摸。
振荡器、 样本及频率
任何音乐合成器的声音发电设施的核心是多个振荡器,所以称为,因为它们生成更多或更少周期的振荡波形,在特定的频率和数量。 在生成音乐的声音,通常创建不会发生变化的周期波形的振荡器声音相当无聊。 更有趣的振荡器纳入颤音,颤音或不断变化的音色,而且他们只有大约定期。
一种程序,希望创建使用 XAudio2 振荡器开始通过调用 XAudio2Create 函数。 这提供了一个实现
IXAudio2 接口的对象。 从该对象中,您可以调用 CreateMasteringVoice 只需一次获取实例的 IXAudio2MasteringVoice,其中主要的音频混音器的作用。 只有一个
IXAudio2MasteringVoice 存在的任何时候。 相反,你会一般 CreateSourceVoice 多次调用创建的 IXAudio2SourceVoice 接口的多个实例。 每个这些
IXAudio2SourceVoice 实例可以作为独立的振荡器。 合并多个振荡器伴唱的文书、 合奏或一个完整的乐团。
IXAudio2SourceVoice 对象的创建和提交缓冲区包含一个描述波形的数字序列生成的声音。 这些数字通常称为样本。 他们常常以恒定速率
16 位宽 (CD 音频的标准),和他们来 — — 通常 44,100 Hz (也为 CD 音频标准) 左右。 这种技术具有脉冲代码调制或 PCM 别致的名称。
虽然此序列的样本可以描述十分复杂的波形,往往一个合成器生成一个相当简单的样本流 — — 最常用的方波、 三角波或锯齿 — — 与对应的波形频率 (视为螺距) 和被看作是卷的平均振幅周期。
例如,如果采样速率是 44,100 Hz,而且每个周期的 100 个样本得到逐步较大、 然后较小、 然后负,和背为零的值,由此产生的声音的频率是除以 100 或 441 Hz 的 44,100 — — 频率接近人类的听觉范围的知觉中心。 (440
Hz 的频率是中间 C 以上 A 和被用作一种优化的标准。
IXAudio2SourceVoice 接口继承一个名为 IXAudio2Voice 的 SetVolume 方法并定义其自己命名的 SetFrequencyRatio 的方法。 我特别好奇的这后一种方法,因为它似乎可以提供一种方法来创建生成在变频和麻烦最少的特定周期波形的振荡器。
图
1 显示一个名为实现这一技术的 SawtoothOscillator1 类的大部分。 虽然我使用熟悉的 16 位整数样品用于定义波形,内部 XAudio2 使用 32 位浮点点样品。 对于性能关键的应用程序,您可能需要探索存在浮点和整数之间的性能差异。
图 1 SawtoothOscillator1 类的多
SawtoothOscillator1::SawtoothOscillator1(IXAudio2* pXAudio2) { // Create a source voice WAVEFORMATEX waveFormat; waveFormat.wFormatTag = WAVE_FORMAT_PCM; waveFormat. nChannels = 1; waveFormat. nSamplesPerSec = 44100; waveFormat. nAvgBytesPerSec = 44100 * 2; waveFormat. nBlockAlign = 2; waveFormat.wBitsPerSample = 16; waveFormat.cbSize = 0; HRESULT hr = pXAudio2->CreateSourceVoice(&pSourceVoice, &waveFormat, 0, XAUDIO2_MAX_FREQ_RATIO); if (FAILED(hr)) throw ref new COMException(hr, "CreateSourceVoice failure"); // Initialize the waveform buffer for (int sample = 0; sample < BUFFER_LENGTH; sample++) waveformBuffer[sample] = (short)(65535 * sample / BUFFER_LENGTH - 32768); // Submit the waveform buffer XAUDIO2_BUFFER buffer = {0}; buffer.AudioBytes = 2 * BUFFER_LENGTH; buffer.pAudioData = (byte *)waveformBuffer; buffer.Flags = XAUDIO2_END_OF_STREAM; buffer.PlayBegin = 0; buffer.PlayLength = BUFFER_LENGTH; buffer.LoopBegin = 0; buffer.LoopLength = BUFFER_LENGTH; buffer.LoopCount = XAUDIO2_LOOP_INFINITE; hr = pSourceVoice->SubmitSourceBuffer(&buffer); if (FAILED(hr)) throw ref new COMException(hr, "SubmitSourceBuffer failure"); // Start the voice playing pSourceVoice->Start(); } void SawtoothOscillator1::SetFrequency(float freq) { pSourceVoice->SetFrequencyRatio(freq / BASE_FREQ); } void SawtoothOscillator1::SetAmplitude(float amp) { pSourceVoice->SetVolume(amp); }
在头文件中,基频设置干净地将分为 44,100 的采样率。 从,可这就是该频率的波形的一个周期的长度计算缓冲区的大小:
static const int BASE_FREQ = 441; static const int BUFFER_LENGTH = (44100 / BASE_FREQ);
此外在页眉中文件作为字段该缓冲区的定义是:
short waveformBuffer[BUFFER_LENGTH];
后创建 IXAudio2SourceVoice 对象,Sawtooth-Oscillator1 构造函数与一个周期的锯齿波形缓冲区已满 — — 简单的波形振幅的-32768 从延伸到振幅的 32,767。 指示应永远重复
IXAudio2SourceVoice 提交此缓冲区。
不需要任何进一步的代码,这是永远扮演 441 Hz 锯齿波的振荡器。 这就是伟大的但它不是非常多的用途。为了让
SawtoothOscillator1 有点更多功能,我还包含了 SetFrequency 的一种方法。 此参数是类使用调用 SetFrequencyRatio 的频率。 传递给
SetFrequencyRatio 的值的范围可以从 XAUDIO2_MIN_FREQ_RATIO (或 1/1,024.0) 的浮点型值超过较早前为 CreateSourceVoice 参数指定的最大值。 我用
XAUDIO2_MAX_FREQ_RATIO (或 1,024.0) 这一论点。 人的听觉范围 — — 约 20 Hz 至 20,000 Hz — — 是由这些应用到 441 基频的两个常量定义的边界内好。
缓冲区和回调
我必须承认我是 SetFrequencyRatio 方法的最初有点怀疑。 数字增加和减少的波形频率不是一个简单的任务。 我觉得必须要与通过算法生成的波形进行比较结果。 这是
OscillatorCompare 项目,这是此列的可下载代码背后的动力。
OscillatorCompare 项目包括我已经描述以及一个 SawtoothOscillator2 类的 SawtoothOscillator1 类。 这第二类有一个
SetFrequency 方法,控件类如何动态生成定义波形的样本。 此波形是不断建造在缓冲区中,并提交实时响应回调中的 IXAudio2SourceVoice 对象。
通过实现 IXAudio2VoiceCallback 接口,类可以从 IXAudio2SourceVoice 接收回调。 实现此接口的类的实例然后作为参数传递给
CreateSourceVoice 方法。 SawtoothOscillator2 类实现此接口本身和它传递给 CreateSourceVoice,也表明它不会做出的 SetFrequencyRatio
使用它自己的实例:
pXAudio2->CreateSourceVoice(&pSourceVoice, &waveFormat, XAUDIO2_VOICE_NOPITCH, 1.0f, this);
实现 IXAudio2VoiceCallback 的类,可以使用 OnBufferStart 方法时它是提交一个新的波形数据缓冲区的时候收到通知。 一般时使用
OnBufferStart 来使波形数据保持最新,您会想要保持一双缓冲区和替换它们。 如果您从另一个源例如音频文件获得音频数据,这可能是最好的解决办法。 目标是不要让音频处理器成为"饿"。保持前处理缓冲区有助于防止饥饿,但并不能保证它。
但我被吸引向由 IXAudio2VoiceCallback 定义的另一种方法 — — OnVoiceProcessingPassStart。 除非您正在用很小的缓冲区,一般
OnVoiceProcessingPassStart 比 OnBufferStart 更频繁调用,并指示时将要处理的音频数据区块和需要多少字节。 在 XAudio2 文档中,此回调方法被促进作为一个最低的延迟,这通常是非常可取的交互式的电子音乐仪器。 你不想按下一个键,听说明之间的延迟
!
SawtoothOscillator2 头文件定义两个常量:
static const int BUFFER_LENGTH = 1024; static const int WAVEFORM_LENGTH = 8192;
第一个常数是缓冲区的用来提交波形数据的长度。 这里它充当一个环形缓冲区。 对
OnVoiceProcessingPassStart 方法的调用请求特定数目的字节为单位)。 该方法通过将这些字节放在缓冲区 (从它掉最后一次离开的地方开始) 和 SubmitSourceBuffer
只呼吁该更新部分缓冲区的响应。 您想要此缓冲区,必须足够大,因此您的程序代码不覆盖仍正在发挥在后台缓冲区的一部分。
原来与 44,100 Hz 采样率的声音,对 OnVoiceProcessingPassStart 的调用总是请求 882 字节或 441 16 位样品。 换句话说,OnVoiceProcessingPassStart
被称为恒定速度的 100 倍每秒,或每 10 毫秒。 虽然没有记载,此 10 ms 持续时间可以视为 XAudio2 音频处理"量程,",它是一个好的数字,要牢记。 因此,对于这种方法编写的代码不能磨蹭。 避免
API 调用和运行时库调用。
第二个常数是波形的所需的一个周期的长度。 它可能是一个数组,包含的波形,样本的大小,但在
SawtoothOscillator2 中它仅用于计算。
SawtoothOscillator2 中的 SetFrequency 方法使用该常量来计算所需波形的频率成正比的角度增量:
angleIncrement = (int)(65536.0 * WAVEFORM_LENGTH * freq / 44100.0);
虽然 angleIncrement 是一个整数,它被处理,就好像它包括整数和小数部分组成的字。 这是波形的用来确定每个连续示例的值。
例如,假设 SetFrequency 的参数是 440 Hz。 AngleIncrement
将计算为 5,356,535。 以十六进制格式,这是 0x51BBF7,这被视为整数 0x51 (或十进制的 81),与 0xBBF7,相当于 0.734 小数部分。 如果波形的完整周期是
8,192 字节和使用唯一的整数部分和跳过 81 字节每个样本的由此产生的频率是约 436.05 hz 的频率。 (那是除以 8,192 44,100 倍 81)。如果您跳过 82 字节,由此产生的频率是
441.43 Hz。 你想要这两个频率之间的事。
这就是为什么还需要输入计算分数的部分。 整件事情可能会更容易在浮点数,和浮动点甚至可能更快一些现代的处理器,但图
2 显示更多的"传统"仅使用整数的方法。 每次调用 SubmitSourceBuffer 指定的环形缓冲区条,更新的通知。
图 2 在 SawtoothOscillator2 OnVoiceProcessingPassStart
void _stdcall SawtoothOscillator2::OnVoiceProcessingPassStart(UINT32 bytesRequired) { if (bytesRequired == 0) return; int startIndex = index; int endIndex = startIndex + bytesRequired / 2; if (endIndex <= BUFFER_LENGTH) { FillAndSubmit(startIndex, endIndex - startIndex); } else { FillAndSubmit(startIndex, BUFFER_LENGTH - startIndex); FillAndSubmit(0, endIndex % BUFFER_LENGTH); } index = (index + bytesRequired / 2) % BUFFER_LENGTH; } void SawtoothOscillator2::FillAndSubmit(int startIndex, int count) { for (int i = startIndex; i < startIndex + count; i++) { pWaveformBuffer[i] = (short)(angle / WAVEFORM_LENGTH - 32768); angle = (angle + angleIncrement) % (WAVEFORM_LENGTH * 65536); } XAUDIO2_BUFFER buffer = {0}; buffer.AudioBytes = 2 * BUFFER_LENGTH; buffer.pAudioData = (byte *)pWaveformBuffer; buffer.Flags = 0; buffer.PlayBegin = startIndex; buffer.PlayLength = count; HRESULT hr = pSourceVoice->SubmitSourceBuffer(&buffer); if (FAILED(hr)) throw ref new COMException(hr, "SubmitSourceBuffer"); }
SawtoothOscillator1 和 SawtoothOscillator2 可以在 OscillatorCompare 程序并行相比。 网页有两对滑块控件更改的频率和每个振荡器的卷。 该频率的滑块控件生成仅
24 至 132 的整数值。 我借来的音乐设备数字接口 (MIDI) 标准用于表示球场的代码从这些值。 24
的值对应于 C 低于中间 C,变桨科学记数法叫做 C 1 (八度 1 C),并有约 32.7 赫兹频率,其中的三个八度。 132 的值对应于 C 10、 中东-C,以上六个八度和约 16,744
Hz 的频率。 关于这些滑块工具提示转换器科学音调符号和等效频率显示的当前值。
因为我在尝试用这些两个振子,我听不清的区别。 我还在直观地检查所产生的波形,另一台计算机上安装软件示波器和我也看不到任何差异。 这表明我的
SetFrequency-比率方法实现智能化,当然我们应该期望在一个系统中复杂的 DirectX。 我怀疑插上重新取样后的波形数据以移频正在执行。 如果你感到紧张,您可以设置
BASE_FREQ 非常低 — — 例如,至 20 赫兹 — — 和类将生成详细的波形,由组成的 2,205 样品。 您还可以尝试以较高的值:例如,8,820 Hz 将导致波形的只是五个样品要生成
! 当然,这有一个有些不同的声音,因为插值的波形介于之间锯齿波和三角波,但由此产生的波形是仍平稳无"锯齿"。
这并不意味着一切正常福。 与任一锯齿振荡器、
顶几个八度获得相当混乱。 波形的采样往往会发出高过之前,听到的一种低频率色彩和,打算在将来更充分调查。
压低音量 !
SetVolume 方法定义的 IXAudio2Voice 和 IXAudio2SourceVoice 的继承记录作为浮点乘数,可以设置为值范围从-2 ^24 至 2 ^24,这等于 16777216。
现实生活中,但是,您可能要将音量上一个 IXAudio2SourceVoice 对象保持为 0 和 1 之间的值。 值对应于沉默的
0 和 1 对应于没有增益或衰减。 请记住无论波形的源关联的 IXAudio2SourceVoice — — 正在通过算法生成还是来自的音频文件 — — 它可能已很有可能接近的-32768
和 32767 的最小和最大值的 16 位样本。 如果您尝试放大这些波形音量级别大于 1 时,样品将超过一个 16 位整数的宽度和将剪切的最小和最大值。 将导致失真和噪声。
当你开始组合 IXAudio2SourceVoice 的多个实例时,这一点非常重要。 这些多个实例的波形都被加在一起的混合。 如果您允许每个这些实例以拥有量的
1,声音的总和很可能导致超出 16 位整数的大小的样本。 偶尔可能发生此错误 — — 只间歇性的失真导致 — — 或慢性病,结果一件很麻烦的。
当使用多个生成完整 16 位宽波形的 IXAudio2SourceVoice 实例,一项安全措施将每个振荡器的卷设置为的声音数除以 1。 保证总和不能超过
16 位的值。 此外可以通过掌握的声音作出整体的卷调整。 你还可能想要看看
XAudio2CreateVolumeMeter 函数,它使您可以创建一个音频处理对象,可以帮助监视卷用于调试目的。
我们第一次的乐器
它是常见的乐器上片有钢琴式键盘,但我过了很久最近由类型的按钮键盘手风琴等俄罗斯巴彦 (其中我所熟悉的俄罗斯作曲家 Sofia Gubaidulina 工作) 上找到。 因为每个键是一个按钮,而不是长的杠杆,太多钥匙能装在
tablet 屏幕,在有限的空间内所示图 3。
图 3 ChromaticButtonKeyboard 程序
底部两行重复前两行上的键和提供,以纾缓共同和弦和旋律序列的指法。 否则,每个组的前三行中的
12 键提供所有备注的倍频程,一般按升序从左到右。 在这里的总范围是大小的四个八度,这是大小的两次什么你与钢琴键盘相同。
真正的巴彦有额外的倍频程,但我不能使按钮太小,不适合。 源代码中允许您设置常数来尝试这额外的倍频程,或消除另一个倍频程,并使按钮甚至更大。
因为我不能说这个计划听起来像是在现实世界中存在的任何文书,我只是叫它 ChromaticButton-键盘。 密钥是为
Key,从 ContentControl 派生,但执行一些触摸处理,以维持一个 IsPressed 属性,生成一个 IsPressedChanged 事件的自定义控件的实例。 当你扫你的手指在键盘处理此控件中的触摸和触摸处理的普通按钮
(其中也有一个 IsPressed 属性) 之间的差异是明显:标准按钮将设置 IsPressed 属性设置为 true 只手指按时发生在表面的按钮,此自定义键控制认为如果手指扫在从一侧按键。
该程序创建六个是从较早的项目几乎完全相同的 SawtoothOscillator1 类的 SawtoothOscillator 类的实例。如果您的触摸屏支持,您可以播放六同时注意到。 有没有回调,并由调用
SetFrequencyRatio 方法控制振荡器的频率。
要跟踪哪些振荡器可用以及哪个振荡器正在玩的 MainPage.xaml.h 文件定义了两个标准的集合对象作为字段:
std::vector<SawtoothOscillator *> availableOscillators; std::map<int, SawtoothOscillator *> playingOscillators;
每个键对象原本的标记属性设置为我前面讨论的 MIDI 注释代码。 这就是
IsPressedChanged 处理程序如何确定哪个键被按下,和什么频率来计算。 MIDI 代码也被用作 playingOscillators 集合的映射键。 直到我玩了重复说明已在播放,导致重复键和异常的底部两个行的一份说明,它运转正常。 通过将一个值,指示关键所在的行的
Tag 属性纳入轻松地解决了这个问题:标记现在等于 MIDI 注代码加行号的 1000 倍。
图
4 显示关键实例的 IsPressedChanged 处理程序。 当按下键时,振荡器是从 availableOscillators 集合中移除,给定频率和非零的卷,并投入
playingOscillators 集合。 当释放某个键时,振荡器提供零卷,并搬回了 availableOscillators。
图 4 为关键实例的 IsPressedChanged 处理程序
void MainPage::OnKeyIsPressedChanged(Object^ sender, bool isPressed) { Key^ key = dynamic_cast<Key^>(sender); int keyNum = (int)key->Tag; if (isPressed) { if (availableOscillators.size() > 0) { SawtoothOscillator* pOscillator = availableOscillators.back(); availableOscillators.pop_back(); double freq = 440 * pow(2, (keyNum % 1000 - 69) / 12.0); pOscillator->SetFrequency((float)freq); pOscillator->SetAmplitude(1.0f / NUM_OSCILLATORS); playingOscillators[keyNum] = pOscillator; } } else { SawtoothOscillator * pOscillator = playingOscillators[keyNum]; if (pOscillator != nullptr) { pOscillator->SetAmplitude(0); availableOscillators.push_back(pOscillator); playingOscillators.erase(keyNum); } } }
这就是简单以及 multi-voice 的文书可以,当然它有缺陷:声音应该不会关闭和打开交换机相似。 卷应滑行起来迅速顺利时注意启动,而且后退时,它将停止。 许多真实文书也说明随着卷和音色的变化。 还有很多改进的余地。
但考虑到代码的简洁性,它竟然行之有效和响应迅速。 如果您编译为
ARM 处理器的程序,可以将它部署上的基于 ARM 的 Microsoft Surface 和周围玩上它与另一只手,而我必须说是有点兴奋时搂着一只手臂在无约束平板电脑走。
Charles
Petzold 是 MSDN 杂志和作者的"Windows 编程,第 6 版"长期贡献 (O‘Reilly 媒体,2012年),一本关于编写应用程序的 Windows 8 书。 他的网站是 charlespetzold.com。
衷心感谢以下技术专家对本文的审阅:Tom Mathews and Thomas Petchel