10

I'm looking for some information on how to play a midi file on IOS. I dont need any midi in or out messages. I simply would like to read the midi file and play the track back to the user substituting each note for a piano sound sample. Being able to adjust the tempo would be another requirement.

Please note that i'm not interest in converting the midi file to wav or another format. I would like to read the midi file directly.

Could anybody point me in the direction of some information that may help me understand the process required.

Cheers

user346443
  • 4,672
  • 15
  • 57
  • 80
  • You don't want any midi out messages, and you don't want to convert the midi to sampled sound either? What _do_ you want to do? – hmakholm left over Monica Aug 25 '11 at 16:16
  • He (or she but, let's face it, probably he) wants to use MIDI messages to control a built-in software synthesizer in iOS. He's asking if such a thing exists. – SSteve Aug 25 '11 at 18:21
  • It doesn't look like iOS 4 has built-in MIDI playback. Here are a couple 3rd-party options: http://stackoverflow.com/questions/4240391 – SSteve Aug 25 '11 at 18:25

5 Answers5

13

I too needed this functionality. Here is code for a skeleton parser that parses MIDI file data provided in an NSData object (e.g. from NSData:dataWithContentsOfFile) and writes what it finds to a mutable string log. A real application would process the various events in a more useful manner, but this should be a good starting point for anyone that needs to parse standard MIDI files as it deals with most of the pain points.

        // MidiParser.h

        #import <Foundation/Foundation.h>

        typedef enum tagMidiTimeFormat
        {
            MidiTimeFormatTicksPerBeat,
            MidiTimeFormatFramesPerSecond
        } MidiTimeFormat;

        @interface MidiParser : NSObject 
        {
            NSMutableString *log;
            NSData *data;
            NSUInteger offset;

            UInt16 format;
            UInt16 trackCount;
            MidiTimeFormat timeFormat;

            UInt16 ticksPerBeat;
            UInt16 framesPerSecond;
            UInt16 ticksPerFrame;
        }

        @property (nonatomic, retain) NSMutableString *log;

        @property (readonly) UInt16 format;
        @property (readonly) UInt16 trackCount;
        @property (readonly) MidiTimeFormat timeFormat;

        - (BOOL) parseData: (NSData *) midiData;

        @end

    //  MidiParser.m

#import "MidiParser.h"

#define kFileCorrupt @"File is corrupt"
#define kInvalidHeader @"Invalid MIDI header"
#define kInvalidTrackHeader @"Invalid Track header"

#define MAIN_HEADER_SIZE 6

#define META_SEQUENCE_NUMBER    0x0
#define META_TEXT_EVENT         0x1
#define META_COPYRIGHT_NOTICE   0x2
#define META_TRACK_NAME         0x3
#define META_INSTRUMENT_NAME    0x4
#define META_LYRICS             0x5
#define META_MARKER             0x6
#define META_CUE_POINT          0x7
#define META_CHANNEL_PREFIX     0x20
#define META_END_OF_TRACK       0x2f
#define META_SET_TEMPO          0x51
#define META_SMPTE_OFFSET       0x54
#define META_TIME_SIGNATURE     0x58
#define META_KEY_SIGNATURE      0x59
#define META_SEQ_SPECIFIC       0x7f

#define CHANNEL_NOTE_OFF        0x8
#define CHANNEL_NOTE_ON         0x9
#define CHANNEL_NOTE_AFTERTOUCH 0xA
#define CHANNEL_CONTROLLER      0xB
#define CHANNEL_PROGRAM_CHANGE  0xC
#define CHANNEL_AFTERTOUCH      0xD
#define CHANNEL_PITCH_BEND      0xE

#define MICRO_PER_MINUTE        60000000

@implementation MidiParser

@synthesize log;

@synthesize format;
@synthesize trackCount;
@synthesize timeFormat;

- (void) dealloc
{
    [log release];
    log = nil;

    [super dealloc];
}

- (UInt32) readDWord
{
    UInt32 value = 0;
    [data getBytes:&value range:NSMakeRange(offset, sizeof(value))];
    value = CFSwapInt32BigToHost(value);
    offset += sizeof(value);
    return value;
}

- (UInt16) readWord
{
    UInt16 value = 0;
    [data getBytes:&value range:NSMakeRange(offset, sizeof(value))];
    value = CFSwapInt16BigToHost(value);
    offset += sizeof(value);
    return value;
}

- (UInt8) readByte
{
    UInt8 value = 0;
    [data getBytes:&value range:NSMakeRange(offset, sizeof(value))];
    offset += sizeof(value);
    return value;
}

- (UInt8) readByteAtRelativeOffset: (UInt32) o
{
    UInt8 value = 0;
    [data getBytes:&value range:NSMakeRange(offset + o, sizeof(value))];
    return value;
}

- (UInt32) readVariableValue
{
    UInt32 value = 0;

    UInt8 byte;
    UInt8 shift = 0;
    do 
    {
        value <<= shift;
        [data getBytes:&byte range:NSMakeRange(offset, 1)];
        offset++;
        value |= (byte & 0x7f);
        shift = 7;
    } while ((byte & 0x80) != 0);

    return value;
}

- (NSString *) readString: (int) length
{
    char *buffer = malloc(length + 1);
    memcpy(buffer, ([data bytes] + offset), length);
    buffer[length] = 0x0;
    NSString *string = [NSString stringWithCString:buffer encoding:NSASCIIStringEncoding];
    free(buffer);
    return string;
}

- (void) readMetaSequence
{
    UInt32 sequenceNumber = 0;
    sequenceNumber |= [self readByteAtRelativeOffset:0];
    sequenceNumber <<= 8;
    sequenceNumber |= [self readByteAtRelativeOffset:1];
    [self.log appendFormat:@"Meta Sequence Number: %d\n", sequenceNumber];
}

- (void) readMetaTextEvent: (UInt32) length
{
    NSString *text = [self readString:length];
    [self.log appendFormat:@"Meta Text: %@\n", text];
}

- (void) readMetaCopyrightNotice: (UInt32) length
{
    NSString *text = [self readString:length];
    [self.log appendFormat:@"Meta Copyright: %@\n", text];
}

- (void) readMetaTrackName: (UInt32) length
{
    NSString *text = [self readString:length];
    [self.log appendFormat:@"Meta Track Name: %@\n", text];
}

- (void) readMetaInstrumentName: (UInt32) length
{
    NSString *text = [self readString:length];
    [self.log appendFormat:@"Meta Instrument Name: %@\n", text];
}

- (void) readMetaLyrics: (UInt32) length
{
    NSString *text = [self readString:length];
    [self.log appendFormat:@"Meta Text: %@\n", text];
}

- (void) readMetaMarker: (UInt32) length
{
    NSString *text = [self readString:length];
    [self.log appendFormat:@"Meta Marker: %@\n", text];    
}

- (void) readMetaCuePoint: (UInt32) length
{
    NSString *text = [self readString:length];
    [self.log appendFormat:@"Meta Cue Point: %@\n", text];    
}

- (void) readMetaChannelPrefix
{
    UInt8 channel = [self readByteAtRelativeOffset:0];
    [self.log appendFormat:@"Meta Channel Prefix: %d\n", channel];
}

- (void) readMetaEndOfTrack
{
    [self.log appendFormat:@"Meta End of Track\n"];
}

- (void) readMetaSetTempo
{
    UInt32 microPerQuarter = 0;
    microPerQuarter |= [self readByteAtRelativeOffset:0];
    microPerQuarter <<= 8;
    microPerQuarter |= [self readByteAtRelativeOffset:1];
    microPerQuarter <<= 8;
    microPerQuarter |= [self readByteAtRelativeOffset:2];

    UInt32 bpm = MICRO_PER_MINUTE / microPerQuarter;
    [self.log appendFormat:@"Meta Set Tempo: Micro Per Quarter: %d, Beats Per Minute: %d\n", microPerQuarter, bpm];
}

- (void) readMetaSMPTEOffset
{
    UInt8 byte = [self readByteAtRelativeOffset:0];
    UInt8 hour = byte & 0x1f;
    UInt8 rate = (byte & 0x60) >> 5;
    UInt8 fps = 0;
    switch(rate)
    {
        case 0: fps = 24; break;
        case 1: fps = 25; break;
        case 2: fps = 29; break;
        case 3: fps = 30; break;
        default: fps = 0; break;
    }
    UInt8 minutes = [self readByteAtRelativeOffset:1];
    UInt8 seconds = [self readByteAtRelativeOffset:2];
    UInt8 frame = [self readByteAtRelativeOffset:3];
    UInt8 subframe = [self readByteAtRelativeOffset:4];
    [self.log appendFormat:@"Meta SMPTE Offset (%d): %2d:%2d:%2d:%2d:%2d\n", fps, hour, minutes, seconds, frame, subframe];
}

- (void) readMetaTimeSignature
{
    UInt8 numerator = [self readByteAtRelativeOffset:0];
    UInt8 denominator = [self readByteAtRelativeOffset:1];
    UInt8 metro = [self readByteAtRelativeOffset:2];
    UInt8 thirty_seconds = [self readByteAtRelativeOffset:3];

    [self.log appendFormat:@"Meta Time Signature: %d/%.0f, Metronome: %d, 32nds: %d\n", numerator, powf(2, denominator), metro, thirty_seconds];
}

- (void) readMetaKeySignature
{
    UInt8 value = [self readByteAtRelativeOffset:0];
    UInt8 accidentals = value & 0x7f;
    BOOL sharps = YES;
    NSString *accidentalsType = nil;
    if((value & 0x80) != 0)
    {
        accidentalsType = [NSString stringWithString:@"Flats"];
        sharps = NO;
    }
    else
    {
        accidentalsType = [NSString stringWithString:@"Sharps"];
    }
    UInt8 scale = [self readByteAtRelativeOffset:1];
    NSString *scaleType = nil;
    if(scale == 0)
    {
        scaleType = [NSString stringWithString:@"Major"];
    }
    else
    {
        scaleType = [NSString stringWithString:@"Minor"];
    }
    [self.log appendFormat:@"Meta Key Signature: %d %@ Type: %@\n", accidentals, accidentalsType, scaleType];
}

- (void) readMetaSeqSpecific: (UInt32) length
{
    [self.log appendFormat:@"Meta Event Sequencer Specific: - Length: %d\n", length];
}

- (void) readNoteOff: (UInt8) channel parameter1: (UInt8) p1 parameter2: (UInt8) p2
{
    [self.log appendFormat:@"Note Off (Channel %d): %d, Velocity: %d\n", channel, p1, p2];
}

- (void) readNoteOn: (UInt8) channel parameter1: (UInt8) p1 parameter2: (UInt8) p2
{
    [self.log appendFormat:@"Note On (Channel %d): %d, Velocity: %d\n", channel, p1, p2];
}

- (void) readNoteAftertouch: (UInt8) channel parameter1: (UInt8) p1 parameter2: (UInt8) p2
{
    [self.log appendFormat:@"Note Aftertouch (Channel %d): %d, Amount: %d\n", channel, p1, p2];
}

- (void) readControllerEvent: (UInt8) channel parameter1: (UInt8) p1 parameter2: (UInt8) p2
{
    [self.log appendFormat:@"Controller (Channel %d): %d, Value: %d\n", channel, p1, p2];
}

- (void) readProgramChange: (UInt8) channel parameter1: (UInt8) p1
{
    [self.log appendFormat:@"Program Change (Channel %d): %d\n", channel, p1];
}

- (void) readChannelAftertouch: (UInt8) channel parameter1: (UInt8) p1
{
    [self.log appendFormat:@"Channel Aftertouch (Channel %d): %d\n", channel, p1];
}

- (void) readPitchBend: (UInt8) channel parameter1: (UInt8) p1 parameter2: (UInt8) p2
{
    UInt32 value = p1;
    value <<= 8;
    value |= p2;
    [self.log appendFormat:@"Pitch Bend (Channel %d): %d\n", channel, value];
}

- (BOOL) parseData:(NSData *)midiData
{
    BOOL success = YES;
    self.log = [[[NSMutableString alloc] init] autorelease];

    @try 
    {
        // Parse data
        data = midiData;
        offset = 0;

        // If size is less than header size, then abort
        NSUInteger dataLength = [data length];
        if((offset + MAIN_HEADER_SIZE) > dataLength)
        {
            NSException *ex = [NSException exceptionWithName:kFileCorrupt 
                                                      reason:kFileCorrupt userInfo:nil];
            @throw ex;
        }

        // Parse header
        if(memcmp([data bytes], "MThd", 4) != 0)
        {
            NSException *ex = [NSException exceptionWithName:kFileCorrupt  
                                                      reason:kInvalidHeader userInfo:nil];
            @throw ex;
        }
        offset += 4;

        UInt32 chunkSize = [self readDWord];
        [self.log appendFormat:@"Header Chunk Size: %d\n", chunkSize];

        // Read format
        format = [self readWord];
        [self.log appendFormat:@"Format: %d\n", format];

        // Read track count
        trackCount = [self readWord];
        [self.log appendFormat:@"Tracks: %d\n", trackCount];

        // Read time format
        UInt16 timeDivision = [self readWord];
        if((timeDivision & 0x8000) == 0)
        {
            timeFormat = MidiTimeFormatTicksPerBeat;
            ticksPerBeat = timeDivision & 0x7fff;
            [self.log appendFormat:@"Time Format: %d Ticks Per Beat\n", ticksPerBeat];
        }
        else
        {
            timeFormat = MidiTimeFormatFramesPerSecond;
            framesPerSecond = (timeDivision & 0x7f00) >> 8;
            ticksPerFrame = (timeDivision & 0xff);
            [self.log appendFormat:@"Time Division: %d Frames Per Second, %d Ticks Per Frame\n", framesPerSecond, ticksPerFrame];
        }

        // Try to parse tracks
        UInt32 expectedTrackOffset = offset;
        for(UInt16 track = 0; track < trackCount; track++)
        {
            if(offset != expectedTrackOffset)
            {
                [self.log appendFormat:@"Track Offset Incorrect for Track %d - Offset: %d, Expected: %d", track, offset, expectedTrackOffset];
                offset = expectedTrackOffset;
            }

            // Parse track header
            if(memcmp([data bytes] + offset, "MTrk", 4) != 0)
            {
                NSException *ex = [NSException exceptionWithName:kFileCorrupt  
                                                          reason:kInvalidTrackHeader userInfo:nil];
                @throw ex;
            }
            offset += 4;

            UInt32 trackSize = [self readDWord];
            expectedTrackOffset = offset + trackSize;
            [self.log appendFormat:@"Track %d : %d bytes\n", track, trackSize];

            UInt32 trackEnd = offset + trackSize;
            UInt32 deltaTime;
            UInt8 nextByte = 0;
            UInt8 peekByte = 0;
            while(offset < trackEnd)
            {
                deltaTime = [self readVariableValue];
                [self.log appendFormat:@"  (%05d): ", deltaTime];

                // Peak at next byte
                peekByte = [self readByteAtRelativeOffset:0];

                // If high bit not set, then assume running status
                if((peekByte & 0x80) != 0)
                {
                    nextByte = [self readByte];
                }

                // Meta event
                if(nextByte == 0xFF)
                {
                    UInt8 metaEventType = [self readByte];
                    UInt32 metaEventLength = [self readVariableValue];
                    switch (metaEventType) 
                    {
                        case META_SEQUENCE_NUMBER:
                            [self readMetaSequence];
                            break;

                        case META_TEXT_EVENT:
                            [self readMetaTextEvent: metaEventLength];
                            break;

                        case META_COPYRIGHT_NOTICE:
                            [self readMetaCopyrightNotice: metaEventLength];
                            break;

                        case META_TRACK_NAME:
                            [self readMetaTrackName: metaEventLength];
                            break;

                        case META_INSTRUMENT_NAME:
                            [self readMetaInstrumentName: metaEventLength];
                            break;

                        case META_LYRICS:
                            [self readMetaLyrics: metaEventLength];
                            break;

                        case META_MARKER:
                            [self readMetaMarker: metaEventLength];
                            break;

                        case META_CUE_POINT:
                            [self readMetaCuePoint: metaEventLength];
                            break;

                        case META_CHANNEL_PREFIX:
                            [self readMetaChannelPrefix];
                            break;

                        case META_END_OF_TRACK:
                            [self readMetaEndOfTrack];
                            break;

                        case META_SET_TEMPO:
                            [self readMetaSetTempo];
                            break;

                        case META_SMPTE_OFFSET:
                            [self readMetaSMPTEOffset];
                            break;

                        case META_TIME_SIGNATURE:
                            [self readMetaTimeSignature];
                            break;

                        case META_KEY_SIGNATURE:
                            [self readMetaKeySignature];
                            break;

                        case META_SEQ_SPECIFIC:
                            [self readMetaSeqSpecific: metaEventLength];
                            break;

                        default:
                            [self.log appendFormat:@"Meta Event Type: 0x%x, Length: %d\n", metaEventType, metaEventLength];
                            break;
                    }

                    offset += metaEventLength;
                }
                else if(nextByte == 0xf0)
                {
                    // SysEx event
                    UInt32 sysExDataLength = [self readVariableValue];
                    [self.log appendFormat:@"SysEx Event - Length: %d\n", sysExDataLength];
                    offset += sysExDataLength;
                }
                else
                {
                    // Channel event
                    UInt8 eventType = (nextByte & 0xF0) >> 4;
                    UInt8 channel = (nextByte & 0xF);
                    UInt8 p1 = 0;
                    UInt8 p2 = 0;

                    switch (eventType) 
                    {
                        case CHANNEL_NOTE_OFF:
                            p1 = [self readByte];
                            p2 = [self readByte];
                            [self readNoteOff: channel parameter1: p1 parameter2: p2];
                            break;

                        case CHANNEL_NOTE_ON:
                            p1 = [self readByte];
                            p2 = [self readByte];
                            [self readNoteOn:channel parameter1:p1 parameter2:p2];
                            break;

                        case CHANNEL_NOTE_AFTERTOUCH:
                            p1 = [self readByte];
                            p2 = [self readByte];
                            [self readNoteAftertouch:channel parameter1:p1 parameter2:p2];
                            break;

                        case CHANNEL_CONTROLLER:
                            p1 = [self readByte];
                            p2 = [self readByte];
                            [self readControllerEvent:channel parameter1:p1 parameter2:p2];
                            break;

                        case CHANNEL_PROGRAM_CHANGE:
                            p1 = [self readByte];
                            [self readProgramChange:channel parameter1:p1];
                            break;

                        case CHANNEL_AFTERTOUCH:
                            p1 = [self readByte];
                            [self readChannelAftertouch:channel parameter1:p1];
                            break;

                        case CHANNEL_PITCH_BEND:
                            p1 = [self readByte];
                            p2 = [self readByte];
                            [self readPitchBend:channel parameter1:p1 parameter2:p2];
                            break;

                        default:
                            break;
                    }

                }
            }
        }

    }
    @catch (NSException *exception) 
    {
        success = NO;
        [self.log appendString:[exception reason]];
    }

    return success;
}

@end
Michael A. McCloskey
  • 2,391
  • 16
  • 19
  • 1
    This is great! You should make a framework for it and put it on github. :) – Nik Reiman Oct 29 '11 at 09:00
  • @Michael McCloskey I'm trying to use your code as a starting point for a project I'm porting, and three errors came up when I brought it into XCode. [log release]; [super dealloc]; self.log = [[[NSMutableString alloc] init] autorelease]; //(2nd [ ) Should I just switch out of automatic reference counting mode? – kcoul Feb 03 '12 at 09:38
  • That code is pre-ARC. You could converting that one file to ARC mode via the Edit\Refactor\Convert to Objectivce C ARC... menu option. I haven't converted my stuff to ARC yet. – Michael A. McCloskey Feb 10 '12 at 20:12
  • Wow! thanks for the code. Would you mind explaining what `- (UInt32) readVariableValue` does? There are many bitwise operations in there. – Daniel Node.js Jul 26 '12 at 12:14
  • In a very delayed reply to Dan the Man, that method is dealing with the way the MIDI data is structured to have the meaningful data in the lower seven bits of each byte with the eighth bit being a flag bit of sorts. I honestly don't remember if I wrote that or ported from another language, but I think that's what's going on in there. – Michael A. McCloskey Feb 11 '14 at 21:38
4

Simply read the MIDI file into a MusicSequence.

This code is from the PlaySequence example in the Apple Docs. http://developer.apple.com/library/mac/#samplecode/PlaySequence/Listings/main_cpp.html

Look at MusicPlayer and MusicTrack too.

OSStatus LoadSMF(const char *filename, MusicSequence& sequence, MusicSequenceLoadFlags loadFlags)
{
    OSStatus result = noErr;
    CFURLRef url = NULL;

    ca_require_noerr (result = NewMusicSequence(&sequence), home);

    url = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, (const UInt8*)filename, strlen(filename), false);

    ca_require_noerr (result = MusicSequenceFileLoad (sequence, url, 0, loadFlags), home);

home:
    if (url) CFRelease(url);
    return result;
}
Gene De Lisa
  • 3,628
  • 1
  • 21
  • 36
4

It doesn't seem that there are any major frameworks out there to aid in reading MIDI files, so your best bet is to roll your own. Here are some resources to get you started:

Nik Reiman
  • 39,067
  • 29
  • 104
  • 160
0

It's been a while since this was asked so there are a few relevant frameworks these days. One way for example is to use AudioKit. It has parts that build on Apple's existing MIDI stuff as well as some more custom classes in development.

Frankie Simon
  • 721
  • 1
  • 7
  • 14
-1

I know this is an old conversation, but it still comes up in searches. The MIDI file parser I wrote as part of my MIDI utilities package should work on pretty much any platform with a C compiler, including iOS.

divbyzero
  • 11
  • 2