Welcome to Pete Brown's 10rem.net

First time here? If you are a developer or are interested in Microsoft tools and technology, please consider subscribing to the latest posts.

You may also be interested in my blog archives, the articles section, or some of my lab projects such as the C64 emulator written in Silverlight.

(hide this)

Interfacing the Novation Launchpad S with a Windows Store app

Pete Brown - 06 January 2015

In this post, I use the NuGet MIDI Preview package to show in Windows 8.1 an early look of what we're doing in Windows 10 for MIDI. I also create an early version of a Launchpad wrapper class to make it easy to use the Launchpad from your own apps.

Following on from my earlier post on MIDI device enumeration (and add/remove detection) I've started building the code to interface with the Novation Launchpad S.

Test app

Here's a screen shot of the app. This is the same app as the previous article. Only the Launchpads are shown, but that's because I'm moving my office and have all my other equipment in storage.

You select an input port and an output port, and then click the "Connect to Launchpad" button. That then calls code that handles MIDI initialization, and event handler wireup.

image 

(The MIDI device names are not helpful right now. We're working on that for Windows 10.)

Once you've connected to the Launchpad, you click "Do Cool Stuff". Static images don't help here, so this is a brief video showing the results of the code:

The Launchpad actually includes a sysex routing for animating a string of text. Pretty nifty!

Now to the meat of the project, the Launchpad class.

Launchpad class

There's a lot here.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Devices.Enumeration;
using WindowsPreview.Devices.Midi;
using System.Runtime.InteropServices.WindowsRuntime;

namespace LaunchpadTest.Devices
{
    abstract class PadEventArgs : EventArgs
    {
        public byte PadNumber { get; private set; }
        public IMidiMessage Message { get; private set; }

        public PadEventArgs(byte padNumber, IMidiMessage message)
        {
            PadNumber = padNumber;
            Message = message;
        }
    }

    class PadPressedEventArgs : PadEventArgs
    {
        public byte Velocity { get; private set; }

        public PadPressedEventArgs(byte padNumber, byte velocity, IMidiMessage message)
            : base(padNumber, message)
        {
            Velocity = velocity;
        }
    }

    class PadReleasedEventArgs : PadEventArgs
    {
        public PadReleasedEventArgs(byte padNumber, IMidiMessage message)
            : base(padNumber, message)
        {
        }
    }

    enum PadMappingMode
    {
        XY = 0x00,
        DrumRack = 0x01
    }

    enum BufferingMode
    {
        Simple = 0x20,
        Buffered0 = 0x24,
        Buffered1 = 0x21,
        Buffered0PlusCopy = 0x34,
        Buffered1PlusCopy = 0x31,
        Flash = 0x28
    }

    enum LedIntensity
    {
        Dim = 0x7D,
        Normal = 0x7E,
        Brightest = 0x7F
    }

    enum KnownPadColors
    {
        Off = 0x0C,
        DimRed = 0x0D,
        MediumRed = 0x0E,
        FullRed = 0x0F,
        DimAmber = 0x1D,
        Yellow = 0x3E,
        FullAmber = 0x3F,
        DimGreen = 0x1C,
        MediumGreen = 0x2C,
        FullGreen = 0x3C,
    }

    // yes, I just mangled the English language for these
    enum TextScrollingSpeed
    {
        Slowest = 0x01,
        Slower = 0x02,
        Slow = 0x03,
        Normal = 0x04,
        Fast = 0x05,
        Faster = 0x06,
        Fastest = 0x07
    }

    /// <summary>
    /// NOTE: Works with Launchpad S, not original Launchpad
    /// TBD if works with Launchpad Mini. Not tested.
    /// </summary>
    class LaunchpadInterface :IDisposable
    {
        public event EventHandler<PadPressedEventArgs> PadPressed;
        public event EventHandler<PadReleasedEventArgs> PadReleased;
        public event EventHandler TextScrollingComplete;

        private const byte InputMidiChannel = 0;
        private const byte OutputMidiChannel = 0;

        private MidiInPort _midiIn;
        private MidiOutPort _midiOut;

        private PadMappingMode _currentMappingMode;

        public void InitializeMidi(MidiDeviceInformation midiInToPC, MidiDeviceInformation midiOutToLaunchpad)
        {
            InitializeMidi(midiInToPC.Id, midiOutToLaunchpad.Id);
        }

        public void InitializeMidi(DeviceInformation midiInToPC, DeviceInformation midiOutToLaunchpad)
        {
            InitializeMidi(midiInToPC.Id, midiOutToLaunchpad.Id);
        }

        public async void InitializeMidi(string midiInToPCDeviceId, string midiOutToLaunchpadDeviceId)
        {
            // acquire the MIDI ports

            // TODO: Exception handling

            _midiIn = await MidiInPort.FromIdAsync(midiInToPCDeviceId);
            _midiIn.MessageReceived += OnMidiInMessageReceived;

            _midiOut = await MidiOutPort.FromIdAsync(midiOutToLaunchpadDeviceId);

            SetPadMappingMode(PadMappingMode.XY);
        }

        private void OnMidiInMessageReceived(MidiInPort sender, MidiMessageReceivedEventArgs args)
        {
            // handle incoming messages
            // these are USB single-device connections, so we're not going to do any filtering

            if (args.Message is MidiNoteOnMessage)
            {

                var msg = args.Message as MidiNoteOnMessage;

                if (msg.Velocity == 0)
                {
                    // note off
                    if (PadReleased != null)
                        PadReleased(this, new PadReleasedEventArgs(msg.Note, msg));
                }
                else
                {
                    // velocity is always 127 on the novation, but still passing it along here
                    // in case they add touch sensitivity in the future

                    // note on
                    if (PadPressed != null)
                        PadPressed(this, new PadPressedEventArgs(msg.Note, msg.Velocity, msg));
                }
            }
            else if (args.Message is MidiControlChangeMessage)
            {
                var msg = args.Message as MidiControlChangeMessage;

                if (msg.Controller == 0 && msg.ControlValue == 3)
                {
                    // this is the notification that text has stopped scrolling
                    if (TextScrollingComplete != null)
                        TextScrollingComplete(this, EventArgs.Empty);
                }
                else
                {
                    System.Diagnostics.Debug.WriteLine("Unhandled MIDI-IN control change message controller: " + msg.Controller + ", value: " + msg.ControlValue);
                }
            }
            else
            {
                System.Diagnostics.Debug.WriteLine("Unhandled MIDI-IN message " + args.Message.GetType().ToString());
            }

        }

        public void ReleaseMidi()
        {
            if (_midiIn != null)
            {
                _midiIn.MessageReceived -= OnMidiInMessageReceived;
                _midiIn.Dispose();
            }

            if (_midiOut != null)
            {
                _midiOut.Dispose();
            }
        }


        // http://d19ulaff0trnck.cloudfront.net/sites/default/files/novation/downloads/4700/launchpad-s-prm.pdf

        /// <summary>
        /// Assumes launchpad is in XY layout mode
        /// </summary>
        /// <param name="row"></param>
        /// <param name="column"></param>
        /// <param name="color"></param>
        public void TurnOnPad(int row, int column, byte color)
        {
            TurnOnPad((byte)(row * 16 + column), color);
        }

        public void TurnOnPad(int row, int column, KnownPadColors color)
        {
            TurnOnPad((byte)(row * 16 + column), color);
        }

        public void TurnOnPad(byte padNumber, byte color)
        {
            if (MidiOutPortValid())
            {
                _midiOut.SendMessage(new MidiNoteOnMessage(OutputMidiChannel, padNumber, color));
            }
        }

        public void TurnOnPad(byte padNumber, KnownPadColors color)
        {
            TurnOnPad(padNumber, (byte)color);
        }


        public static byte RedGreenToColorByte(byte red, byte green, byte flags=0x0C)
        {
            byte color = (byte)(0x10 * (green & 0x03) + (red & 0x03) + flags);

            //System.Diagnostics.Debug.WriteLine("Red: 0x{0:x2}, Green: 0x{1:x2}, Color: 0x{2:x2}", red, green, color);

            return color;
        }

        public void TurnOffPad(byte row, byte column)
        {
            TurnOnPad(row, column, KnownPadColors.Off);
        }

        public void TurnOffPad(byte padNumber)
        {
            TurnOnPad(padNumber, KnownPadColors.Off);
        }

        public void ScrollText(string text, KnownPadColors color, TextScrollingSpeed speed = TextScrollingSpeed.Normal, bool loop = false)
        {
            ScrollText(text, (byte)color, speed, loop);
        }

        public void ScrollText(string text, byte color, TextScrollingSpeed speed = TextScrollingSpeed.Normal, bool loop = false)
        {
            if (MidiOutPortValid())
            {
                var encoding = Encoding.GetEncoding("us-ascii");
                var characters = encoding.GetBytes(text);

                if (loop)
                    color += 64;        // set bit 4 to set looping

                // 
                var header = new byte[] { 0xF0, 0x00, 0x20, 0x29, 0x09, color, (byte)speed};
                var fullData = new byte[characters.Length + header.Length];

                header.CopyTo(fullData, 0);                     // header info, including color
                characters.CopyTo(fullData, header.Length);     // actual text
                fullData[fullData.Length - 1] = 0xF7;           // sysex terminator

                _midiOut.SendMessage(new MidiSystemExclusiveMessage(fullData.AsBuffer()));
            }
        }

        public void StopScrollingText()
        {
            var data = new byte[] { 0xF0, 0x00, 0x20, 0x29, 0x09, 0x00, 0xF7};

            _midiOut.SendMessage(new MidiSystemExclusiveMessage(data.AsBuffer()));

        }


        public void Reset()
        {
            // NOTE: this also changes the mapping mode to the default power-on value
            // We'll have a real problem keeping that in sync

            if (MidiOutPortValid())
            {
                _midiOut.SendMessage(new MidiControlChangeMessage(OutputMidiChannel, 0x00, 0x00));

                _currentMappingMode = PadMappingMode.XY;
            }
        }

        public void TurnOnAllPads(LedIntensity intensity)
        {
            if (MidiOutPortValid())
            {
                _midiOut.SendMessage(new MidiControlChangeMessage(OutputMidiChannel, 0x00, (byte)intensity));
            }
        }

        public void SetPadMappingMode(PadMappingMode mode)
        {
            if (MidiOutPortValid())
            {
                _midiOut.SendMessage(new MidiControlChangeMessage(OutputMidiChannel, 0x00, (byte)mode));

                _currentMappingMode = mode;
            }
        }

        public void SetBufferingMode(BufferingMode mode)
        {
            if (MidiOutPortValid())
            {
                _midiOut.SendMessage(new MidiControlChangeMessage(OutputMidiChannel, 0x00, (byte)mode));
            }
        }

        private bool MidiOutPortValid()
        {
            return _midiOut != null;
        }

        private bool MidiInPortValid()
        {
            return _midiIn != null;
        }
    }
}

This class is not complete. For example, I haven't yet implemented the code to light up the top row of buttons, or handle their presses. There's no exception handling, and I've done only minimal testing.

Here are some interesting parts of the code

Events: This class raises separate events for pad pressed and released. This just involves translating MIDI note on and off messages. When I include the code for handling the top row, I'll likely add two more events, or augment the args to include information as to what type of button/pad was pressed.

I also raise and event for the Text Scrolling completed. If you look in the OnMidiMessageReceived function, you can see that I handle a specific control change message specially. The Launchpad sends this specific control change when the text has finished scrolling on the pads.

The ScrollText function uses the MIDI API to send a sysex (System Exclusive) message formatted per the Novation documentation. To stop scrolling the text, you simply send it blank text.

Tip

Our MIDI API just passes through a raw buffer, so you need to add the standard 0xF0 to the start and 0xF7 to the end of the buffer.

Lighting pads: To light a pad on the Launchpad, you send a MIDI Note On message with the appropriate pad number. The Launchpad has bi-color red/green LEDs. Each color can have a two-bit intensity between 0 and 3. This information is packed into specific bit positions in the velocity argument. Here's the format:

Bit Use
6 Leave as 0
5..4 Green LED brightness
3..2 Buffer management
1..0 Red LED brightness

I've provided some constants for common colors, but you can also calculate the colors manually or use the static function I've included in this class. There are more details in the Launchpad S developer's guide.

MainPage Code-behind

In the main page, I've added some code to handle the three buttons, and the notification that text scrolling has completed.

private LaunchpadInterface _launchpad;

private void Connect_Click(object sender, RoutedEventArgs e)
{
    if (OutputDevices.SelectedItem != null && InputDevices.SelectedItem != null)
    {
        if (_launchpad != null)
        {
            _launchpad.Dispose();
            _launchpad = null;
        }

        _launchpad = new LaunchpadInterface();

        _launchpad.PadPressed += _launchpad_PadPressed;
        _launchpad.PadReleased += _launchpad_PadReleased;
        _launchpad.TextScrollingComplete += _launchpad_TextScrollingComplete;

        _launchpad.InitializeMidi(
            (MidiDeviceInformation)InputDevices.SelectedItem, 
            (MidiDeviceInformation)OutputDevices.SelectedItem);
    }
    else
    {
        var msg = new MessageDialog("Please select the appropriate input and output MIDI interfaces.");
        msg.ShowAsync();
    }

}

private void _launchpad_TextScrollingComplete(object sender, EventArgs e)
{
    for (int row = 0; row < 8; row++)
        for (int col = 0; col < 8; col++)
            _launchpad.TurnOnPad((byte)(row * 16 + col),
                LaunchpadInterface.RedGreenToColorByte((byte)(row % 4), (byte)(col % 4)));
}

private void _launchpad_PadReleased(object sender, PadReleasedEventArgs e)
{
    _launchpad.TurnOffPad(e.PadNumber);
}

private void _launchpad_PadPressed(object sender, PadPressedEventArgs e)
{
    _launchpad.TurnOnPad(e.PadNumber, KnownPadColors.FullRed);
}

private void Reset_Click(object sender, RoutedEventArgs e)
{
    if (_launchpad != null)
    {
        _launchpad.Reset();
    }
}

private void CoolStuff_Click(object sender, RoutedEventArgs e)
{
    if (_launchpad != null)
    {
        _launchpad.SetBufferingMode(BufferingMode.Simple);

        _launchpad.ScrollText("Hello World from Windows 8.1!", 
                     KnownPadColors.FullRed, TextScrollingSpeed.Normal, false);
    }
}

This code creates an instance of the launch pad interface class, and wires up handlers. The MIDI interface selection code is identical to the previous blog post.

Note the TextScrollingComplete handler. The sequence of events is to display the scrolling text, and then display a gradient of sorts across the Launchpad pads. The text display happens in CoolStuff_Click, the gradient happens in the event handler.

Summary

Controlling the Launchpad from the Windows 8.1 app was pretty easy. As you can imagine, this is part of a larger project I have in mind. This app is just a test harness, but it has been good for proving out MIDI enumeration and the Launchpad interface.

This may work with the old Launchpad and the Launchpad Mini. I haven't yet tried them out, but will try out the Mini at least. I also intend to look at a couple of the other controllers (especially the Launch Control and Launch Control XL) to see if they make sense for what I plan to do with them.

When we release Windows 10 to developers, I'll make any updates to the Launchpad and MIDI enumeration code and then post it on GitHub for all to use.

The code for this project, in its current state, is available below. As before, you'll need Visual Studio 2015 to load and compile the project.

Links

       
posted by Pete Brown on Tuesday, January 6, 2015
filed under:        

3 comments for “Interfacing the Novation Launchpad S with a Windows Store app”

  1. Jean-Gabriel Perromatsays:
    Hi,

    I try to receive message from my midi device but it does not work, I see the device in the enumeration, but the messagereceived event is never raised. Can you help me please ?

    private void OnMidiInMessageReceived(MidiInPort sender, MidiMessageReceivedEventArgs args)
    {

    MidiNoteOnMessage MidiMsg;

    switch (args.Message.Type)
    {
    case MidiMessageType.NoteOn:
    MidiMsg = args.Message as MidiNoteOnMessage;
    WEng.Note_On(MidiMsg.Note, MidiMsg.Velocity, 0);
    break;

    case MidiMessageType.NoteOff:
    MidiMsg = args.Message as MidiNoteOnMessage;
    WEng.Note_Off(MidiMsg.Note);
    break;

    default:
    break;
    }

    }

    public async void InitializeMidiIn(string MidiInDeviceId)
    {
    if (MidiInput != null) MidiInput.Dispose();
    MidiInput = await MidiInPort.FromIdAsync(MidiInDeviceId);
    MidiInput.MessageReceived += OnMidiInMessageReceived;
    }

Comment on this Post

Remember me