Introduction
Most MIDI messages are hearable, but others only control some settings on the
MIDI device. This article describes how to abuse the message "Program Change",
which changes the instrument4s sound, to hide a short message in the MIDI
file.
What it is NOT about
The article is not about encryption, it is only about hiding. Secret messages
should be encrypted before they are hidden in an unsuspicious media file, but
that step is not a part of this text.
Short MIDI Overview
A MIDI file contains events. Every event consists of a time, a message type
and a specific number of parameters (data bytes). There are eight possible
types:
Type |
Name |
Data |
Meaning |
80 |
Note Off |
2 Bytes (Note, Velocity) |
The musician starts playing a note |
90 |
Note On |
2 Bytes (Note, Velocity) |
The musician stops playing a note |
A0 |
After Touch |
2 Bytes (Note, Pressure) |
The pressure on a key changes between 90 and 80 |
B0 |
Control Change |
2 Bytes (Control, Value) |
A device specific setting is changed |
C0 |
Program change |
1 Byte (Program Number) |
Another program ("instrument") is selected |
D0 |
Channel Pressure |
1 Byte (Pressure) |
After Touch for a whole channel used by devices without sensors on every
key |
E0 |
Pitch Wheel |
2 Bytes (combined to a 14-bit value) |
The pitch wheel setting changes |
F0 |
System Exclusive |
all Bytes to next 0xF7 |
Device dependent messages |
The lower four bits are reserved for the channel number. If you play a middle
C on channel 5, your sequencer (the MIDI keyboard) sends a message like
that:
02 94 3C B7
Two clocks from start, key on channel 4 (counted from 0), note 60 (middle C),
velocity 92.
If you switch the "instrument" to "Piano" before you start playing, it sends
a message like that:
00 C4 00
Before start of song, program change on channel 5, new program is number
0.
When the sequencer saves the recorded messages, it places a header at the
beginning of the file, and headers at the beginning of each track. Every header
contains two fields for type and length.
struct ChunkHeader {
char[] type;
Int32 length;
}
The type can be "MThd" for a file header, or MTrk for a track header. A file
header looks like that:
After the file header, the first track header has to follow. A typical file
header looks like that:
The length of a track header specifies the count of bytes until the next
track header begins. These bytes are system and MIDI messages. System messages
have the type 0xFF, a subtype byte, and a length byte. The length specifies the
count of data bytes:
Usually a file starts with a couple of non-midi messages, followed by Control
Change messages, and then Program Change and Note On/Off messages:
The end of every track is marked by an End Of Track event:
If you want to know everything about the MIDI specification, I recommend you
visit the MIDI Technical
Fanatic4s Brainwashing Center.
Silent Hiding Places
What happens, if a few Program Change messages follow in a row, without a
Note On/Off message between them? The MIDI device changes from on program to
another in no time, reaches the last one and then plays the next sound. You
don't hear the Program Change itself, you just hear the notes played with the
current program. That means we can hide a Program Change message BEFORE another
Program Change message, and nobody is going to hear it.
The data byte, which contains the program number to switch to, can be any
value from 0 to 127. Note that bit #7 of each octet is reserved as a Start Of
Message flag. All message types have bit #7 set to 1, all other bytes use only
bits #0 to #6. Variable length fields (time fields and parameters of SysEx
messages) don4t need a length field, because they end with the first byte
>127 (this must be the start of the next message).
The Program Change messages are good for hiding a short message, but the
bytes of an Unicode text may be >127. So we have to split the bytes. There
are more than enough bits for hiding an half byte in one Program Change.
Splitting a byte is easy:
private byte[] SplitByte(byte b){
byte[] parts = new byte[2];
parts[0] = (byte)(b >> 4);
parts[1] = (byte)((byte)(b << 4) >> 4);
return parts;
}
All we have to do is to step through the MIDI file until we reach a Program
Change event, insert a copy of that event with the program number changed to the
half byte we want to hide, and then look for the next Program Change event to
hide the next half byte. An average MIDI file contains less Program Change
events than an average sentence contains letters, so we have to place multiple
fake events before the original one.
Before we begin, it is good to define a few structures. They will make things
easier.
public struct MidiFileHeader {
public char[] HeaderType;
public byte[] DataLength;
public Int16 FileType;
public Int16 CountTracks;
public Int16 Division;
}
public struct MidiTrackHeader {
public char[] HeaderType;
public Int32 DataLength;
}
public struct MidiMessage {
public byte[] Time;
public byte MessageType;
public byte[] MessageData;
public MidiMessage(MidiMessage template, byte[] messageData){
Time = template.Time;
MessageType = template.MessageType;
MessageData = messageData;
}
}
Now we can start reading a MIDI file. All the security checks about if the
file contains enough messages and its size won't increase too much are left out,
you can view them in the complete source files.
public void HideOrExtract(String srcFileName, String dstFileName,
Stream secretMessage, Stream key, bool extract){
FileStream srcFile = new FileStream(srcFileName, FileMode.Open);
srcReader = new BinaryReader(srcFile);
dstWriter = null;
if(dstFileName != null){
FileStream dstFile = new FileStream(dstFileName, FileMode.Create);
dstWriter = new BinaryWriter(dstFile);
}
bool isMessageComplete = false;
MidiMessage midiMessage = new MidiMessage();
MidiFileHeader header = new MidiFileHeader();
header.HeaderType = CopyChars(4);
header.DataLength = new byte[4];
header.DataLength = CopyBytes(4);
if((new String(header.HeaderType) != "MThd")
||(header.DataLength[3] != 6)){
MessageBox.Show("This is not a standard MIDI file!");
srcReader.Close();
dstWriter.Close();
return;
}
header.FileType = (Int16)(CopyByte()*16 + CopyByte());
header.CountTracks = (Int16)(CopyByte()*16 + CopyByte());
header.Division = (Int16)(CopyByte()*16 + CopyByte());
Now that we passed the file header, we expect to find the first track header.
It is time to read the first pair of secret half-bytes, and then dive deep into
the track.
byte[] currentMessageByte = extract
? new byte[2]{0,0}
: SplitByte((byte)secretMessage.ReadByte());
byte currentMessageByteIndex = 0;
Int32 countBytesAdded = 0;
int countIgnoreMessages = GetKeyByte(key);
for(int track=0; track<header.CountTracks; track++){
if(srcReader.BaseStream.Position == srcReader.BaseStream.Length){
break;
}
MidiTrackHeader th = new MidiTrackHeader();
th.HeaderType = CopyChars(4);
if(new String(th.HeaderType) != "MTrk"){
while(srcReader.BaseStream.Position+4 < srcReader.BaseStream.Length){
th.HeaderType = CopyChars(4);
if(new String(th.HeaderType) == "MTrk"){
break;
}
}
}
int trackLengthPosition = (dstWriter == null) ? 0
: (int)dstWriter.BaseStream.Position;
byte[] trackLength = new byte[4];
trackLength = CopyBytes(4);
th.DataLength = trackLength[0] << 24;
th.DataLength += trackLength[1] << 16;
th.DataLength += trackLength[2] << 8;
th.DataLength += trackLength[3];
The header is done, let's continue with the messages. Usually the first
messages are non-MIDI information, like song name and lyrics. We can copy them
into the destination file without looking at the content.
bool isEndOfTrack = false;
countBytesAdded = 0;
while( ! isEndOfTrack){
ReadMidiMessageHeader(ref midiMessage);
if(midiMessage.MessageType == 0xFF){
if(dstWriter != null){
dstWriter.Write(midiMessage.Time);
dstWriter.Write(midiMessage.MessageType);
}
byte name = CopyByte();
int length = (int)CopyVariableLengthValue();
CopyBytes(length);
if((name == 0x2F)&&(length == 0)){
isEndOfTrack = true;
}
}
The MIDI messages are more interesting. We have to remove the channel number
(lower four bits) to get the message type, then we can check if we have found a
Program Change.
else{
byte cleanMessageType = (byte)(((byte)(midiMessage.MessageType >> 4))
<< 4);
if((cleanMessageType != 0xC0)&&(dstWriter != null)){
dstWriter.Write(midiMessage.Time);
dstWriter.Write(midiMessage.MessageType);
}
switch(cleanMessageType){
case 0x80:
case 0x90:
case 0xA0:
case 0xB0:
case 0xD0:
case 0xE0:{
CopyBytes(2);
break;
}
case 0xF0: {
byte b=0;
while(b != 0xF7){
b = CopyByte();
}
break;
}
case 0xC0:{
We have found a Program Change message. Depending on the total count of
Program Changes, we have to hide one or more 4-bit-packets here ("block size").
Extracting the message later on we have to know that block size, so it4ll be the
first information to hide, and the first information to extract.
midiMessage.MessageData = srcReader.ReadBytes(1);
if( ! isHalfBytesPerMidiMessageFinshed){
if(extract){
halfBytesPerMidiMessage = midiMessage.MessageData[0];
countBytesAdded -= midiMessage.Time.Length + 2;
ReadMidiMessageHeader(ref midiMessage);
midiMessage.MessageData = srcReader.ReadBytes(1);
}else{
MidiMessage msg = new MidiMessage(midiMessage,
new byte[1]{halfBytesPerMidiMessage});
WriteMidiMessage(msg);
countBytesAdded += midiMessage.Time.Length + 2;
}
isHalfBytesPerMidiMessageFinshed = true;
}
ProcessMidiMessage(midiMessage, secretMessage, key, extract,
ref isMessageComplete, ref countIgnoreMessages,
ref currentMessageByte, ref currentMessageByteIndex,
ref countBytesAdded);
break;
}
}}}
Have we forgotten anything? Yes, we have added messages to the track, so the
length field is incorrect now. We have to return to the header and overwrite the
old length.
if(dstWriter != null){
th.DataLength += countBytesAdded;
trackLength = IntToArray(th.DataLength);
dstWriter.Seek(trackLengthPosition, SeekOrigin.Begin);
dstWriter.Write(trackLength);
dstWriter.Seek(0, SeekOrigin.End);
}
}
}
Now it is really time to get to the point of hiding the secret message. The
method ProcessMidiMessage
only decides whether to hide or extract,
and calls ProcessMidiMessageH
for hiding or
ProcessMidiMessageE
for extracting data.
ProcessMidiMessageH
hides a couple of blocks and then copies the
original MIDI event:
...
for(int n=0; n<halfBytesPerMidiMessage; n++){
MidiMessage msg = new MidiMessage(midiMessage,
new byte[midiMessage.MessageData.Length]);
isMessageComplete = HideHalfByte(msg, secretMessage,
ref currentMessageByte, ref currentMessageByteIndex,
ref countBytesAdded);
if(isMessageComplete){ break; }
}
...
WriteMidiMessage(midiMessage);
...
private bool HideHalfByte(MidiMessage midiMessage, Stream secretMessage,
ref byte[] currentMessageByte, ref byte currentMessageByteIndex,
ref int countBytesAdded){
bool returnValue = false;
midiMessage.MessageData[0] = currentMessageByte[currentMessageByteIndex];
WriteMidiMessage(midiMessage);
countBytesAdded += midiMessage.Time.Length + 1 +
midiMessage.MessageData.Length;
currentMessageByteIndex++;
if(currentMessageByteIndex == 2){
int nextValue = secretMessage.ReadByte();
if(nextValue < 0){
returnValue = true;
}else{
currentMessageByte = SplitByte( (byte)nextValue );
currentMessageByteIndex = 0;
}
}
return returnValue;
}
That4s all we need to hide information in a MIDI file. Quite simple, isn4t
it? ProcessMidiMessageH
reverses the process:
...
for(int n=0; n<halfBytesPerMidiMessage; n++){
ExtractHalfByte(midiMessage, secretMessage,
ref currentMessageByte, ref currentMessageByteIndex,
ref countBytesAdded);
if((secretMessage.Length==8)&&(secretMessageLength==0)){
secretMessage.Seek(0, SeekOrigin.Begin);
byte[] bytes = new byte[8];
secretMessage.Read(bytes, 0, 8);
secretMessageLength = ArrayToInt(bytes);
secretMessage.SetLength(0);
}
else if((secretMessageLength > 0)&&(secretMessage.Length==
secretMessageLength)){
isMessageComplete = true;
break;
}
if((n+1)<halfBytesPerMidiMessage){
ReadMidiMessageHeader(ref midiMessage);
midiMessage.MessageData = srcReader.ReadBytes(1);
}
}
...
private void ExtractHalfByte(MidiMessage midiMessage, Stream secretMessage,
ref byte[] currentMessageByte, ref byte currentMessageByteIndex,
ref int countBytesAdded){
currentMessageByte[currentMessageByteIndex] = midiMessage.MessageData[0];
countBytesAdded -= midiMessage.Time.Length + 1 +
midiMessage.MessageData.Length;
currentMessageByteIndex++;
if(currentMessageByteIndex == 2){
byte completeMessageByte = (byte)((currentMessageByte[0]<<4) +
currentMessageByte[1]);
secretMessage.WriteByte(completeMessageByte);
currentMessageByte[0]=0;
currentMessageByte[1]=0;
currentMessageByteIndex = 0;
}
}
Conversions between Big-Endian and Little-Endian
Maybe you have noticed the methods IntToArray
and
ArrayToInt
. These two functions convert integers between the
Little-Endian format used by C# and the Big-Endian byte arrays we read from and
write to MIDI files. For example, in a MIDI file the Int16 value 12345 is stored
as "0x30 0x39". The higher byte is stored left from the lower byte! C# expects
the higher byte to be right from the lower byte, it stores integer values from
low to high. That4s why you can not use functions like
BinaryReader.ReadInt16
. You can use ReadChars
and
ReadBytes
, but everything else would turn the byte order upside
down. No problem, you can read integer values byte by byte, and then shift all
the bytes into one integer variable:
public static byte[] IntToArray(Int64 val){
byte[] bytes = new byte[8];
for(int n=0; n<8; n++){
bytes[n] = (byte)(val >> (n*8));
}
return bytes;
}
public Int64 ArrayToInt(byte[] bytes){
Int64 result = 0;
for(int n=0; n<bytes.Length; n++){
result += (bytes[n] << (n*8));
}
return result;
}