Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reading Midi files on IOS

Tags:

ios

midi

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

like image 756
user346443 Avatar asked Aug 25 '11 16:08

user346443


2 Answers

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
like image 149
Michael A. McCloskey Avatar answered Oct 15 '22 23:10

Michael A. McCloskey


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:

  • http://www.sonicspot.com/guide/midifiles.html
  • http://www.gweep.net/~prefect/eng/reference/protocol/midispec.html
  • http://home.roadrunner.com/~jgglatt/
like image 24
Nik Reiman Avatar answered Oct 15 '22 21:10

Nik Reiman