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

适用于 UWP 应用的简单软件 MIDI 键盘

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2017年3月28日

CPOL

12分钟阅读

viewsIcon

14710

使用 Microsoft GS Wavetable Synth 的简单软件 MIDI 键盘

引言

去年我创建了一个 UWP 趣味应用 Play Your Solo,用于 Windows 应用商店。为此,我需要在应用中内置一个具有不同乐器的软件键盘。我搜索了网络上的教程和代码示例,但没有找到任何适用于 C# 的最新内容。

作为一名 C# 编程和 Visual Studio 使用的初学者,如果我能从一个好的示例中进行一些复制粘贴,完成这个应用会更快。尽管这并不复杂,而且我从自己动手创建中也学到了很多。

对于那些希望以简单的方式创建软件 MIDI 键盘作为应用一部分,或者将其作为 MIDI 世界的入门训练项目的人来说,我创建了这个小示例。它没有完成的触摸效果,因为 XAML 部分并不那么有趣。

背景

作为 MIDI 格式和内置 Microsoft GS Wavetable Synth 的初学者,我在 Windows Dev Center 上找到了一份文档 MIDI,它提供了程序的基本设置。这份文档易于阅读和理解。

为了更好地理解 MIDI 消息,我找到了来自卡内基梅隆大学计算机科学学院的 Dominique Vandenneucker 的 这个 页面。关于通用 MIDI (GM) 中使用的乐器,这个 页面非常有帮助。这是斯坦福大学计算机辅助人文研究中心音乐课程的讲义。

使用代码

首先,启动 Visual Studio 2015。必须安装 Windows 10 SDK。创建一个新项目,选择“空白应用(通用 Windows)”模板。将项目命名为 MidiKeyBoard。

项目构建完成后,您需要引用用于内置 MIDI 合成器的 SDK 扩展。为此,在 Visual Studio 的解决方案资源管理器中右键单击“引用”,然后选择“添加引用”。

会弹出一个新窗口。在该窗口中,展开“通用 Windows”节点,然后单击“扩展”。

根据您的 Visual Studio 配置,将显示一个更长的可用扩展列表。选择“Microsoft General MIDI DLS for Universal Windows Apps”扩展。

如果存在多个同名扩展,请选择与您的应用目标匹配的版本。要检查应用的目標版本,请转到项目属性并选择“应用程序”选项卡。

单击“确定”,即可添加扩展。

现在已添加用于内置 MIDI 合成器的 SDK 扩展,是时候进行简单的 MIDI 键盘布局了。首先,将应用锁定为横向模式,因为在纵向模式下键盘会显得奇怪且小。

为此,在解决方案资源管理器中双击 Package.appxmanifest。选择“应用程序”选项卡,然后勾选“仅横向”。这将把屏幕锁定在横向模式。

Lock app in Landscape mode

MainPage.xaml 中,为主网格创建三个行:一个用于状态行,一个用于键盘,一个用于选择乐器。

将网格的背景颜色设置为黄色,以便能看清键盘的黑键和白键。

状态栏将包含一个 TextBlock,用于显示是否连接了任何 MIDI 设备。应如下所示:

<Page
    x:Class="MidiKeyboard.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:MidiKeyboard"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="Yellow">

        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <TextBlock Name="StatusTextBlock"
                   Grid.Row="0"
                   Margin="10" />
    </Grid>
</Page>

在代码隐藏文件 MainPage.xaml.cs 中,添加命名空间 Windows.Devices.EnumerationWindows.Devices.Midi,以便能够枚举设备和 MIDI 设备。另外,添加 System.Threading.Tasks 命名空间,以便在异步方法中使用 Task。

...
using Windows.Devices.Enumeration;
using Windows.Devices.Midi;
System.Threading.Tasks;
...

为了找到 Microsoft GS Wavetable Synth 并连接到它,通过枚举连接到 Windows 10 设备的所有 MIDI 输出设备。这是通过 DeviceInformation 类的 FindAllAsync 方法完成的。创建一个私有方法 EnumerateMidiOutputDevices()

EnumerateMidiOutputDevices() 方法中,创建一个字符串来保存 FindAllAsync 方法的查询信息。查询将包含获取所有已连接 MIDI 输出端口的信息。查询字符串将用作 FindAllAsync 方法的输入,搜索结果将收集在 DeviceInformationCollection 对象中。

如果没有找到 MIDI 输出设备,则从方法返回,并将 StatusTextBlock 中的文本设置为“未找到 MIDI 输出设备”。如果找到 MIDI 输出设备(希望是 Microsoft GS Wavetable Synth),则设备名称将显示在 StatusTextBlock 中。在此示例中,假设没有连接外部 MIDI 输出设备,因此 MIDI 输出设备集合将只有一个项,即所需的 Microsoft GS Wavetable Synth。

由于 EnumerateMidiOutputDevices() 是一个返回 Task 的异步方法,因此不应直接从 MainPage() 调用它。创建一个名为 GetMidiOutputDevicesAsync() 的方法来调用 EnumerateMidiOutputDevices()GetMidiOutputDevicesAsync() 应从 MainPage() 调用。它看起来像这样:

MainPage 方法

        public MainPage()
        {
            this.InitializeComponent();

            // Get the MIDI output device
            GetMidiOutputDevicesAsync();
        }

GetMidiOutputDevicesAsync:

        /// <summary>
        /// Method to call EnumarateMidiOutputDevice from the MainPage().
        /// EnumerateMidiOutputDevices() has to be called via a await call, and that cannot be done from the constructor.
        /// </summary>
        private async void GetMidiOutputDevicesAsync()
        {
            await EnumerateMidiOutputDevices();
        }

EnumerateMidiOutputDevices 方法

/// <summary>
/// 
/// </summary>
/// <returns></returns>
private async Task EnumerateMidiOutputDevices()
{
    // Create the query string for finding all MIDI output devices using MidiOutPort.GetDeviceSelector()
    string midiOutportQueryString = MidiOutPort.GetDeviceSelector();

    // Find all MIDI output devices and collect it in a DeviceInformationCollection using FindAllAsync
    DeviceInformationCollection midiOutputDevices = await DeviceInformation.FindAllAsync(midiOutportQueryString);

    // Return if no external devices are connected
    if (midiOutputDevices.Count == 0)
    {
        // Set the StatusTextBlock foreground color to red
        StatusTextBlock.Foreground = new SolidColorBrush(Colors.Red);

        // Set the StatusTextBlock text to "No MIDI output devices found"
        StatusTextBlock.Text = "No MIDI output devices found";
        return;
    }
    else
    {
        // Set the StatusTextBlock foreground color to green
        StatusTextBlock.Foreground = new SolidColorBrush(Colors.Green);

        // Set the StatusTextBlock text to the name of the first item in midiOutputDevices collection
        StatusTextBlock.Text = midiOutputDevices[0].Name;
    }
}

现在已经找到 MIDI 输出设备(希望是 Microsoft GS Wavetable Synth),请创建一个 DeviceInformation 实例来保存 Microsoft GS Wavetable Synth 的信息。这将用于使用 DeviceInformationId 属性创建 IMidiOutPort 接口的对象。此对象将用于发送 MIDI 消息以执行操作。因此,在页面的代码隐藏顶部定义 IMidiOutPort 对象,以便它可以在 EnumerateMidiOutputDevices 方法之外使用。

页面代码隐藏的顶部

...
namespace MidiKeyboard
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage : Page
    {
        // IMidiOutPort of the output MIDI device
        IMidiOutPort midiOutPort;
 
        public MainPage()
        {
...

EnumerateMidiOutputDevices

...
    else
    {  
       // Set the StatusTextBlock foreground color to green
       StatusTextBlock.Foreground = new SolidColorBrush(Colors.Green);

       // Set the StatusTextBlock text to the name of the first item in midiOutputDevices collection
       StatusTextBlock.Text = midiOutputDevices[0].Name;
    }

    // Create an instance of DeviceInformation and set it to the first midi device in DeviceInformationCollection, midiOutputDevices
    DeviceInformation devInfo = midiOutputDevices[0];

    // Return if DeviceInformation, devInfo, is null
    if (devInfo == null)
    {
        // Set the midi status TextBlock
        StatusTextBlock.Foreground = new SolidColorBrush(Colors.Red);
        StatusTextBlock.Text = "No device information of MIDI output";
        return;
    }


    // Set the IMidiOutPort for the output midi device by calling MidiOutPort.FromIdAsync passing the Id property of the DeviceInformation
    midiOutPort = await MidiOutPort.FromIdAsync(devInfo.Id);

    // Return if midiOutPort is null
    if (midiOutPort == null)
    {
        // Set the midi status TextBlock
        StatusTextBlock.Foreground = new SolidColorBrush(Colors.Red);
        StatusTextBlock.Text = "Unable to create MidiOutPort from output device";
        return;
     }
}

现在调试时,您可以找到 Microsoft GS Wavetable Synth 的 Id 属性,并在 FromIdAsync 方法中硬编码该 Id。然后,每次加载应用程序时都可以避免使用 FindAllAsync 方法进行枚举。这样做的缺点是,如果 Id 在设备之间或通过更新发生变化,应用程序将停止工作。

要在应用挂起时释放资源,请创建一个方法,在应用挂起时调用。在此方法中,调用 MidiOutPort 的 dispose 方法并将 MidiOutPort 设置为 null。

        /// <summary>
        /// Eventhandler to clean up the MIDI connection, when the app is suspended.
        /// The object midiOutPort is disposed and set to null
        /// </summary>
        /// <param name="sender"><</param>
        /// <param name="e"></param>
        private void App_Suspending(object sender, SuspendingEventArgs e)
        {
            try
            {
                // Dispose the Midi Output port
                midiOutPort.Dispose();

                // Set the midiOutPort to null
                midiOutPort = null;
            }
            catch
            {
                // Do noting. A cleanup has already been made
            }
        }

应用程序暂停后恢复时,应重新建立与 MIDI 输出端口的连接。创建一个方法来调用 EnumerateMidiOutputDevices() 方法来设置 MIDI 连接。

        /// <summary>
        /// Eventhandler to restore connection to the MIDI device when app is resuming after suspension.
        /// The method EnumerateMidiOutputDevices() will be called.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private async void App_Resuming(object sender, object e)
        {
            // Call method to set up output midi device
            await EnumerateMidiOutputDevices();
        }

最后一步是在 MainPage() 中为挂起和恢复事件添加事件处理程序。

...
        public MainPage()
        {
            this.InitializeComponent();

            // Get the MIDI output device
            GetMidiOutputDevicesAsync();

            // Reopen the midi connection when resuming after a suspension
            Application.Current.Resuming += new EventHandler<Object>(App_Resuming);

            // Close the midi connection on suspending the app
            Application.Current.Suspending += new SuspendingEventHandler(App_Suspending);
        }
...

现在已完成使用 Microsoft GS Wavetable Synth 的初始设置,接下来需要设置布局。此布局将非常简单,外观也不那么好看。创建良好的布局时,XAML 代码很容易变得冗长且难以阅读。出于同样的原因,只显示五个音符,从 C 到 E 的半音阶。

MainPage.xaml 中,在主网格中创建名为 KeyboardGrid 的网格元素,并将其设置为主网格的第 1 行。

KeyboardGrid 中,定义五列以容纳键盘上的五个音符。创建五个矩形,放置在每列中。将每个矩形的名称设置为音符的名称:CkeyCSharpKeyDKeyDSharpKeyEKey。设置填充颜色以交替显示白色和黑色。

在每个矩形元素中,创建两个事件来控制键盘。创建一个 PointerPressed 事件,在按下按键时调用 Key_PointerPressed 事件处理程序。创建一个 PointerReleased 事件,在再次释放按键时调用 Key_PointerReleased 事件处理程序。

KeyboardGrid 网格应如下所示:

...
    <Grid Background="Yellow">

        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <TextBlock Name="StatusTextBlock"
                   Grid.Row="0"
                   Margin="10" />

        <Grid Name="KeyboardGrid" 
              Grid.Row="1"
              Margin="20">
            
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

            <Rectangle Name="CKey"
                       Grid.Column="0"
                       Fill="White"
                       PointerPressed="Key_PointerPressed"
                       PointerReleased="Key_PointerReleased"/>

            <Rectangle Name="CSharpKey"
                       Grid.Column="1"
                       Fill="Black"
                       PointerPressed="Key_PointerPressed"
                       PointerReleased="Key_PointerReleased"/>

            <Rectangle Name="DKey"
                       Grid.Column="2"
                       Fill="White"
                       PointerPressed="Key_PointerPressed"
                       PointerReleased="Key_PointerReleased"/>

            <Rectangle Name="DSharpKey"
                       Grid.Column="3"
                       Fill="Black"
                       PointerPressed="Key_PointerPressed"
                       PointerReleased="Key_PointerReleased"/>

            <Rectangle Name="EKey"
                       Grid.Column="4"
                       Fill="White"
                       PointerPressed="Key_PointerPressed"
                       PointerReleased="Key_PointerReleased"/>
        </Grid>

    </Grid>
...

希望当应用程序在 Visual Studio 2015 中运行时,它看起来会像这样:

Key_PointerPressed 事件处理程序中,将发送 MIDI 消息以播放特定音符。与 MIDI 合成器通信的一种方式是通过 IMidiMessage 接口。在此应用中,我们将使用接口中的三种消息:Note On(音符开)、Note Off(音符关)和 Program Change(程序更改)。通用 MIDI (GM) 标准指定了 16 个通道和 128 个预定义乐器。由于 MIDI 键盘一次只能播放一种乐器,因此在此示例中仅使用通道 0。

Program Change 消息用于在特定通道上选择或更改 MIDI 输出设备的乐器。消息格式如下:MidiProgramChangeMessage(byte '通道号', byte '乐器号')。在此示例中,我们使用通道 0,并将乐器设置为 Acoustic Grand Piano,这是预定义乐器列表中的第一个乐器。由于我们在程序中使用列表,因此会从列表中的数字减去 1 来获得正确的乐器。现在,在页面代码隐藏的顶部定义 IMidiMessage,并在 MainPage() 中创建 MidiProgramChangeMessage

它应该看起来像这样

...
    public sealed partial class MainPage : Page
    {
        // MidiOutPort of the output MIDI device
        IMidiOutPort midiOutPort;

        // MIDI message to change the instument
        IMidiMessage instrumentChange;

        public MainPage()
        {
            this.InitializeComponent();

            // Set the initial instrument of the keyboard to Acoustic Grand Piano in channel 0
            instrumentChange = new MidiProgramChangeMessage(0, 0);

            // Get the MIDI output device
            GetMidiOutputDevicesAsync();

            // Reopen the midi connection when resuming after a suspension
            Application.Current.Resuming += new EventHandler<Object>(App_Resuming);

            // Close the midi connection on suspending the app
            Application.Current.Suspending += new SuspendingEventHandler(App_Suspending);
        }
...

要设置初始乐器,请在 EnumerateMidiOutputDevices() 方法的末尾,将创建的 MIDI 消息 instrumentChange 发送到 MidiOutPort midiOutPort

...
            // Return if midiOutPort is null
            if (midiOutPort == null)
            {
                // Set the midi status TextBlock
                StatusTextBlock.Foreground = new SolidColorBrush(Colors.Red);
                StatusTextBlock.Text = "Unable to create MidiOutPort from output device";
                return;
            }

            // Send the Program Change midi message to port of the output midi device
            // to set the initial instrument to Acoustic Grand Piano.
            midiOutPort.SendMessage(instrumentChange);
        }
...

现在已设置 MIDI 输出设备的乐器,必须使键盘正常工作。要从所选乐器开始以选定的频率(音高)发出 MIDI 声音,请发送 MIDI 消息 Note On。要再次停止声音,请发送 MIDI 消息 Note Off。

音高范围从 0 到 127。中央 C 的值为 60,音高间隔为半音。因此,D# 的值为 61,D 的值为 62,依此类推。

与真实乐器一样,有些乐器在音符被弹奏时会保持声音(也许是通过振动或变化),例如小号。有些乐器在音符被弹奏时会使声音衰减,例如钢琴。

MIDI 声音的力度定义了声音的强度。数字越大,力度越大。力度范围从 0 到 127。在此示例中,我们使用力度 127。

Note On 消息的格式为:MidiNoteOnMessage(byte '通道号', byte '音符', byte '力度')

同样,当我们想在特定通道上停止特定音符时,我们使用 MIDI 消息 Note Off。Note Off 消息的格式为:MidiNoteOffMessage(byte '通道号', byte '音符', byte '力度')

由于在此示例中我们始终使用通道 0 和力度 127,因此请在页面代码隐藏的顶部创建它们。

...
    public sealed partial class MainPage : Page
    {
        // MidiOutPort of the output MIDI device
        IMidiOutPort midiOutPort;

        // MIDI message to change the instument
        IMidiMessage instrumentChange;

        // The MIDI channel used for the MIDI output device
        byte channel = 0;

        // The MIDI velocity used for the MIDI output device
        byte velocity = 127;

        public MainPage()
        {
            this.InitializeComponent();
...

Key_PointerPressed 事件处理程序中,我们将使用 switch/case 提取按下的按键,并根据此选择音高。当音高已知时,可以创建带有通道和力度的 Note On 消息,并将消息发送到 MIDI 输出设备。为了能够创建一个 Rectangle 元素,以便转换按下的按键,请添加命名空间 Windows.UI.Xaml.Shapes

Key_PointerPressed 将如下所示:

        /// <summary>
        /// Eventhandler for key pressed at the keyboard.
        /// The specific key is extracted and the according pitch is found.
        /// A MIDI message Note On is created from the channel field and velocity field.
        /// The Note On message is send to the MIDI output device
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Key_PointerPressed(object sender, PointerRoutedEventArgs e)
        {
            // Field to hold the pitch of the note
            byte note;
            
            // Extract the key being pressed
            Rectangle keyPressed = (Rectangle)sender;

            // Get the name of the key and store it in a string, keyPressedName
            string keyPressedName = keyPressed.Name;

            // Switch/Case to set the pitch depending of the key pressed
            switch (keyPressedName)
            {
                case "CKey":
                    note = 60;
                    break;
                case "CSharpKey":
                    note = 61;
                    break;
                case "DKey":
                    note = 62;
                    break;
                case "DSharpKey":
                    note = 63;
                    break;
                case "EKey":
                    note = 64;
                    break;
                default:
                    note = 60;
                    break;
            }

            // Create the Note On message to send to the MIDI output device
            IMidiMessage midiMessageToSend = new MidiNoteOnMessage(channel, note, velocity);

            // Send the Note On MIDI message to the midiOutPort
            midiOutPort.SendMessage(midiMessageToSend);     
        }

要再次关闭特定音符,在 Key_PointerReleased 事件处理程序中使用相同的代码。只是这次创建了一个 Note Off 消息发送到 MIDI 输出设备。

        /// <summary>
        /// Eventhandler for key released at the keyboard.
        /// The specific key is extracted and the according pitch is found.
        /// A MIDI message Note Off is created from the channel field and velocity field.
        /// The Note Off message is send to the MIDI output device
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Key_PointerReleased(object sender, PointerRoutedEventArgs e)
        {
            // Field to hold the pitch of the note
            byte note;

            // Extract the key being pressed
            Rectangle keyPressed = (Rectangle)sender;

            // Get the name of the key and store it in a string, keyPressedName
            string keyPressedName = keyPressed.Name;

            // Switch/Case to set the pitch depending of the key pressed
            switch (keyPressedName)
            {
                case "CKey":
                    note = 60;
                    break;
                case "CSharpKey":
                    note = 61;
                    break;
                case "DKey":
                    note = 62;
                    break;
                case "DSharpKey":
                    note = 63;
                    break;
                case "EKey":
                    note = 64;
                    break;
                default:
                    note = 60;
                    break;
            }

            // Create the Note Off message to send to the MIDI output device
            IMidiMessage midiMessageToSend = new MidiNoteOffMessage(channel, note, velocity);

            // Send the Note Off MIDI message to the midiOutPort
            midiOutPort.SendMessage(midiMessageToSend);
        }

如果应用程序在 Visual Studio 2015 中运行,MIDI 键盘现在应该功能齐全,既适用于使用鼠标或触摸板的计算机,也适用于使用触摸屏的设备。

MIDI 设备能够同时播放多个不同音高的音符。因此,创建的键盘将能够播放复音。只需同时按下多个按键即可创建和弦。

能够选择不同的乐器将更有用。为此,在 XAML 页面中,在主网格的第三行创建一个 StackPanel。StackPanel 应具有水平方向。在 StackPanel 中,创建四个按钮以选择不同的乐器。每个按钮在单击时都应调用相同的事件处理程序。

XAML 页面中的附加代码可能如下所示:

...
        <StackPanel Grid.Row="2"
                    Orientation="Horizontal"
                    Margin="20">
            
            <Button Name="Piano"
                    Margin="0,0,10,0"
                    Content="Piano"
                    Click="Selection_Click"/>

            <Button Name="Trombone"
                    Margin="0,0,10,0"
                    Content="Trombone"
                    Click="Selection_Click"/>

            <Button Name="Trumpet"
                    Margin="0,0,10,0"
                    Content="Trumpet"
                    Click="Selection_Click"/>

            <Button Name="Flute"
                    Margin="0,0,10,0"
                    Content="Flute"
                    Click="Selection_Click"/>

        </StackPanel>
...

在创建 Selection_Click 事件处理程序之前,需要了解一些关于乐器的理论。大多数乐器都有一个自然的频率范围,在这个范围内它们很有用。这个范围取决于乐器中放大声音部分的物理形状。在长号上弹奏非常高的音符听起来并不好。同样,您永远不会在短笛上弹奏低音符。钢琴很特别,因为它具有宽广的频率范围,但在本例中,它被设置为与小号相同。

在目前的示例中,长号的频率范围比小号和钢琴低一个八度。长笛的频率范围比小号和钢琴高一个八度。

MIDI 设备中的一个八度等于音高值中的 12 个步长。为了控制八度间隔,在代码隐藏页面的顶部定义了一个整数。

...
    public sealed partial class MainPage : Page
    {
        // MidiOutPort of the output MIDI device
        IMidiOutPort midiOutPort;

        // MIDI message to change the instument
        IMidiMessage instrumentChange;

        // Integer to define the frequecy interval of an instrument
        int octaveInterval = 0;

        // The MIDI channel used for the MIDI output device
        byte channel = 0;

        // The MIDI velocity used for the MIDI output device
        byte velocity = 127;

        public MainPage()
        {
...

在按钮事件处理程序 Selection_Click 中,创建一个按钮元素以获取所选按钮的名称。按钮的名称将用于选择新的 MIDI 乐器编号和频率间隔,使用 switch/case。选择乐器编号后,将创建一个新的 Program Change 消息并将其发送到 MIDI 输出设备。

Selection_Click() 事件处理程序应如下所示:

        /// <summary>
        /// Eventhandler for the buttons that select the instrument being played.
        /// Each buttons has a different name, that will be used to get a different value of MIDI instrument using a Switch/Case.
        /// A Program Change message is created from that and the MIDI channel and the message is sent to the MIDI output device.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Selection_Click(object sender, RoutedEventArgs e)
        {
            // Define a byte to hold the MIDI instrument number to be used in the Program Change MIDI message
            byte midiInstrument;
            
            // Create a Button element to get the pressed instrument button
            Button instrumentNameButton = (Button)sender;

            // Create a string to hold the name of the pressed button to be used in a switch/case
            string instrumentName = instrumentNameButton.Name;

            // Depending on the name of the button pressed, set a new MIDI instrument and frequency interval for the instrument
            switch (instrumentName)
            {
                case "Piano":
                    // Set the MIDI instrument number to 0, Piano
                    midiInstrument = 0;

                    // Set the octaveInterval to 0 to set the frequency interval around the middle C
                    octaveInterval = 0;
                    break;
                case "Trombone":
                    // Set the MIDI instrument number to 57, Trombone (58-1)
                    midiInstrument = 57;

                    // Set the octaveInterval to -1 to set the frequency interval around one octave lower than the middle C
                    octaveInterval = -1;
                    break;
                case "Trumpet":
                    // Set the MIDI instrument number to 56, Trumpet (57-1)
                    midiInstrument = 56;

                    // Set the octaveInterval to 0 to set the frequency interval around the middle C
                    octaveInterval = 0;
                    break;
                case "Flute":
                    // Set the MIDI instrument number to 73, Trumpet (74-1)
                    midiInstrument = 73;

                    // Set the octaveInterval to 1 to set the frequency interval around one octave higher than the middle C
                    octaveInterval = 1;
                    break;

                // Default value will be equal to piano
                default:
                    // Set the MIDI instrument number to 0
                    midiInstrument = 0;

                    // Set the octaveInterval to 0 to set the frequency interval around the middle C
                    octaveInterval = 0;
                    break;
            }

            // Create the new Program Change message with the new selected instrument
            instrumentChange = new MidiProgramChangeMessage(channel, midiInstrument);

            // Send the Program Change midi message to port of the output midi device.
            midiOutPort.SendMessage(instrumentChange);
        }

最后一步是使弹奏的音符的音高取决于按钮事件处理程序中设置的乐器频率间隔。这可以通过在 Key_PointerPressedKey_PointerReleased 事件处理程序中的音高选择中添加一个部分来完成。该部分简单地是 '12 * octaveInterval',用于将频率间隔提高或降低一个八度。

Key_PointerPressed 事件处理程序现在应如下所示:

...
            // Extract the key being pressed
            Rectangle keyPressed = (Rectangle)sender;

            // Get the name of the key and store it in a string, keyPressedName
            string keyPressedName = keyPressed.Name;

            // Switch/Case to set the pitch depending of the key pressed
            switch (keyPressedName)
            {
                case "CKey":
                    note = (byte)(60 + (octaveInterval * 12));
                    break;
                case "CSharpKey":
                    note = (byte)(61 + (octaveInterval * 12));
                    break;
                case "DKey":
                    note = (byte)(62 + (octaveInterval * 12));
                    break;
                case "DSharpKey":
                    note = (byte)(63 + (octaveInterval * 12));
                    break;
                case "EKey":
                    note = (byte)(64 + (octaveInterval * 12));
                    break;
                default:
                    note = (byte)(60 + (octaveInterval * 12));
                    break;
            }

            // Create the Note On message to send to the MIDI output device
            IMidiMessage midiMessageToSend = new MidiNoteOnMessage(channel, note, velocity);
...

同样,Key_PointerReleased 事件处理程序也应如此:

...
            // Extract the key being pressed
            Rectangle keyPressed = (Rectangle)sender;

            // Get the name of the key and store it in a string, keyPressedName
            string keyPressedName = keyPressed.Name;

            // Switch/Case to set the pitch depending of the key pressed
            switch (keyPressedName)
            {
                case "CKey":
                    note = (byte)(60 + (octaveInterval * 12));
                    break;
                case "CSharpKey":
                    note = (byte)(61 + (octaveInterval * 12));
                    break;
                case "DKey":
                    note = (byte)(62 + (octaveInterval * 12));
                    break;
                case "DSharpKey":
                    note = (byte)(63 + (octaveInterval * 12));
                    break;
                case "EKey":
                    note = (byte)(64 + (octaveInterval * 12));
                    break;
                default:
                    note = (byte)(60 + (octaveInterval * 12));
                    break;
            }

            // Create the Note Off message to send to the MIDI output device
            IMidiMessage midiMessageToSend = new MidiNoteOffMessage(channel, note, velocity);
...

现在,简单的 MIDI 软件键盘示例已完成。

关注点

从这个项目中要记住的一点是,使用 MIDI 格式和 MIDI 设备是多么简单和容易。在我开始创建 MIDI 软件键盘之前,这对我来说有点吓人。下一个项目可以做一个能够创建使用多个通道的 MIDI 旋律的应用程序,并且也能够播放它们。这似乎有点复杂,但希望它会像创建 Note On/Note Off 消息并将它们发送到 MIDI 输出设备一样简单。

历史

  • 初版文章和演示源代码创建时间 20170228
© . All rights reserved.