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

简单的键盘电子琴

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (6投票s)

2014 年 10 月 15 日

CPOL

5分钟阅读

viewsIcon

26419

downloadIcon

641

这是一个简单的电钢琴(钢琴),用于教授如何使用 DirectSound 合成声音。

引言

本文将介绍如何使用数学公式合成简单的声音,并在不使用任何声音文件(如 wav 或 mp3)的情况下播放。所有声音波形都借助 `Math.Sine` 函数合成。这是创建自己的合成器非常第一步。我使用了两种音律系统:“平均律”(也称为“十二平均律”)和早期使用的“毕达哥拉斯调律”音律系统。通过这个程序,每个人都可以听到两种音律系统之间的区别。这个电钢琴可以同时播放多种声音。声音可以通过鼠标或电脑键盘来播放。我想让这个程序尽可能简单,所以这是一个有趣的程序,请大家不要想得太复杂。

背景

要理解这其中的基本原理,研究 Directx-DirectSound 的工作原理非常有用。我在这里找到了一个非常有用的文章 这里。这是一个可用的示例,我在它的帮助下得以开始。关于 DirectSound 工作原理的其他基础知识可以在微软官方页面 这里 找到。虽然有很多关于 DirectSound 的文章,但我特别需要的是 .NET 配合 DirectSound 的文章。许多文章使用 C++,所以当时对我不适用。

另一点是,大家可能会问我如何计算声音的频率。网上或字典里也有很多文章。我认为,大家都知道基本的 'A' 音的频率是 440Hz(每秒周期)(在中世纪也有 415Hz 等)。高一个八度的声音频率会乘以 2,低一个八度则除以 2。计算“平均律”声音的频率相对简单:平均律频率构成一个以 2 的 12 次方根为公比的等比数列。这是一个约等于 1.05946 的常数。所以,在计算时,我们需要将频率乘以这个常数的第一次、第二次、第三次等幂。所有这些都在 这篇文章 中得到了很好的阐述。而毕达哥拉斯音律系统则更难一些。这需要一些音乐知识,例如理解五度音、八度音是什么……以及什么是五度调。

因此,我不想做一节唱歌或音乐课(我不是一个理论音乐家),但我建议阅读这些关于 毕达哥拉斯调律半音阶 的文章,我认为它们也很有趣。我认为这两篇文章不仅仅对音乐家来说是易于理解的。

Using the Code

让我们来看看程序。首先,我创建了一个类:类名为 `Sound`。每个声音都是这个类的代表。该类有两个常量,分别是正常 A 音的频率,以及平均律频率等比数列的公比。它们被声明为 `static`,因为这个常量在类中使用,不仅仅是实例。该类有一个 `private` 类型的 `SecondaryBuffer` 属性,这是将包含合成声音样本的“数组”。类的构造函数可以计算声音样本,并将波形样本放入 `SecondaryBuffer`。(要理解什么是 SecondaryBuffer,你需要理解 DirectSound 的工作原理,所以请阅读我之前提供的文章)。在计算时,我使用正弦数学函数在一个循环中。

一些准备工作

//How many samples we need
int samples = (int)(format.SamplesPerSecond * format.Channels) ;
//Make a short-array with length of number calculated before.
short[] buffer = new short[samples];
//calculate how many bytes we need into buffer
description.BufferBytes = buffer.Length * format.BlockAlign;

样本计算循环

//cycle to calculate sine wave sound from 0 to number of samples
  for (int i = 0; i < buffer.Length; i++)
            {
             //calculate the i-th member of array with Sin function. multiplicate with 2 will make higher amplitude of voice.
                buffer[i] = (short)(AMPL * Math.Sin(2 * Math.PI * i * freq / format.SamplesPerSecond));
            }

`Buffer` 是一个 short 类型的数组,然后我将 buffer 写入 secondary buffer。

sec_buffer.Write(0, buffer, LockFlag.None);

最后,`sound` 类有三个方法,用于启动和停止声音,以及播放我在鼠标点击事件中使用的短促声音。

public void ClickPlay()
{
    sec_buffer.Play(0, BufferPlayFlags.Default);
}

//Let's start to play sound by button pressing
public void StartPlay()
{
   sec_buffer.Play(0, BufferPlayFlags.Looping);        
}
//Let's stop the sound by button releasing
public void StopPlay()
{
    sec_buffer.Stop();
}

如果有人想制作一个中世纪或任何“异域风情”的钢琴:将 NORMALA 常量设置为 415(或任何你想要的值),然后运行程序。声音将是统一的——但不“正常”。

接着,我们来看看 Form。每个声音都是 `Sound` 类的一个全局实例。

Sound c;
Sound cis;
Sound d;
Sound dis;
...

不带 `_p` 后缀的变量代表平均律波形,带 `_p` 后缀的变量代表毕达哥拉斯声音。
`Form_Load` 事件初始化所有声音。当 Form 准备就绪时,所有音色都已计算完成,我使用 `new` 关键字创建(构造)了该类。现在,程序已准备好捕获钢琴键盘上的键盘或鼠标点击事件。我使用简单的按钮来格式化键盘,但我必须注意按钮的放置顺序。第二个按钮会覆盖第一个。这就是为什么按钮编号(按钮 1..2..15 等)有点令人困惑的原因。首先,我做了一些准备工作来初始化 DirectSound,如下所示。要了解细节,请阅读我在“背景”部分提供的关于 DirectSound 的文章。

WaveFormat format = new WaveFormat();
//format tag=pcm
format.FormatTag = WaveFormatTag.Pcm;
//one channel
format.Channels = 1;
//How many bits per sample
format.BitsPerSample = 16;
//how many samples per second of the voice. 44100 like CD quality
format.SamplesPerSecond = 44100;
//Block align calculating
format.BlockAlign = (short)(format.Channels * format.BitsPerSample / 8);
//Bytes per sec calculating
format.AverageBytesPerSecond = format.BlockAlign * format.SamplesPerSecond;

//Create a buffer description with the waveformat
BufferDescription description = new BufferDescription(format);
//Can control the volume
description.ControlVolume = true;

//Defines a Directsound device
Device device = new Device();
//Assign as a new windows control
device.SetCooperativeLevel(new System.Windows.Forms.Control(), CooperativeLevel.Priority);

//Now create the sound-class members. Each sound is a member of Sound class. 
//In Sound's constructor  calculates the exact numbers of sound-samples.
//First creating the well temperated sounds, then Pythagorean.

//********************* Well temperated sounds *************************

//The sounds frequencies are members of a geometric progression with quotient is TEMPER (see below). And I calculate each frequency with multiplicate the  powers of TEMPER.
c = new Sound(Sound.NORMALA / Math.Pow(Sound.TEMPER, 9), format, description, device);

最后一行意味着:我创建了(平均律的)C 音,其频率等于 A 音的频率(440Hz)除以公比的 9 次幂,其中公比是 2 的 12 次方根。这很简单,不是吗?

每个按钮都有一个 `mouseclick` 事件。在 `mouseclick` 事件中,通过 `if` 结构,我选择合适的声音,并调用声音(类成员)的 `ClickPlay` 方法。通过 `checkbox1` 和 `2`,我们可以选择要使用的音律系统。我们可以两者都选:事实上,在这种情况下区别最明显。

if (checkBox1.Checked)
{
    cis.ClickPlay();
}
if (checkBox2.Checked)
{
    cis_p.ClickPlay();
}

最后,我必须捕获键盘事件。在 `key_down` 事件中,声音将开始,而 `key_up` 将停止。在正常情况下,我们无法捕获 Form 的键盘事件,只能捕获组件的键盘事件,例如文本框等,它们可以获得焦点。但 Form 没有焦点。这个问题在 Form-Load 事件中的这一行得到了解决。

this.KeyPreview = true;

在这行代码之后,我们就可以捕获键盘事件了。在 `Form_Keydown` 和 `Form_Keyup` 中,通过 `switch`-`case` 结构,程序会选择正确的音色(当然,我们可以选择平均律或毕达哥拉斯音律,就像在 `mouseclick` 事件中一样),并调用 `Sound` 类的 `StartPlay` 或 `StopPlay` 方法。

//Check whether temperated or diatonic is selected
if (checkBox1.Checked)
{
    //Select the key-events what to do
    switch (ee.KeyCode)
    {
         //In case of A button pressed down, then C sound will start...
        case Keys.A:
            //and the button's colour let's be grey
            button1.BackColor = Color.Silver;
            c.StartPlay();
            break;

`Key_Down` 和 `Key_Up` 事件处理程序非常相似:第一个开始声音,第二个停止。当然,两者都会重新着色按钮。`Key_down` 会使其变为银色,`Key_up` 会恢复原来的黑色或白色。

关注点

通过本文,您可以学习

  • 如何使用 Directx/Directsound
  • 如何仅借助正弦函数合成声音,而无需使用 wav 或 mp3 文件
  • 最后但同样重要的是,一些音乐理论

历史

这是本示例的第一个版本。

© . All rights reserved.