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.
(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