/// <summary>Parses a byte array into a track's worth of events.</summary> /// <param name="data">The data to be parsed.</param> /// <returns>The track containing the parsed events.</returns> public static MidiTrack ParseToTrack(byte[] data) { long pos = 0; // current position in data bool running = false; // whether we're in running status int status = 0; // the current status byte bool sysExContinue = false; // whether we're in a multi-segment system exclusive message byte[] sysExData = null; // system exclusive data up to this point from a multi-segment message // Create the new track MidiTrack track = new MidiTrack(); try { // Process all bytes, turning them into events while (pos < data.Length) { // Read in the delta time long deltaTime = ReadVariableLength(data, ref pos); // Get the next character byte nextValue = data[pos]; // Are we continuing a sys ex? If so, the next value better be 0x7F if (sysExContinue && (nextValue != 0x7F)) { var mpe = new MidiParserException("Expected to find a system exclusive continue byte.", pos); mpe.Data["nextValue"] = nextValue; throw mpe; } // Are we in running status? Determine whether we're running and // what the current status byte is. if ((nextValue & 0x80) == 0) { // We're now in running status... if the last status was 0, uh oh! if (status == 0) { throw new MidiParserException("Status byte required for running status.", pos); } // Keep the last iteration's status byte, and now we're in running mode running = true; } else { // Not running, so store the current status byte and mark running as false status = nextValue; running = false; } // Grab the 4-bit identifier byte messageType = (byte)((status >> 4) & 0xF); MidiEvent tempEvent = null; // Handle voice events if (messageType >= 0x8 && messageType <= 0xE) { if (!running) { pos++; // if we're running, we don't advance; if we're not running, we do } byte channel = (byte)(status & 0xF); // grab the channel from the status byte tempEvent = ParseVoiceEvent(deltaTime, messageType, channel, data, ref pos); } // Handle meta events else if (status == 0xFF) { pos++; byte eventType = data[pos]; pos++; tempEvent = ParseMetaEvent(deltaTime, eventType, data, ref pos); } // Handle system exclusive events else if (status == 0xF0) { pos++; long length = ReadVariableLength(data, ref pos); // figure out how much data to read // If this is single-segment message, process the whole thing if (data[pos + length - 1] == 0xF7) { sysExData = new byte[length - 1]; Array.Copy(data, (int)pos, sysExData, 0, (int)length - 1); tempEvent = new SystemExclusiveMidiEvent(deltaTime, sysExData); } // It's multi-segment, so add the new data to the previously aquired data else { // Add to previously aquired sys ex data int oldLength = (sysExData == null ? 0 : sysExData.Length); byte[] newSysExData = new byte[oldLength + length]; if (sysExData != null) { sysExData.CopyTo(newSysExData, 0); } Array.Copy(data, (int)pos, newSysExData, oldLength, (int)length); sysExData = newSysExData; sysExContinue = true; } pos += length; } // Handle system exclusive continuations else if (status == 0xF7) { if (!sysExContinue) { sysExData = null; } // Figure out how much data there is pos++; long length = ReadVariableLength(data, ref pos); // Add to previously aquired sys ex data int oldLength = (sysExData == null ? 0 : sysExData.Length); byte[] newSysExData = new byte[oldLength + length]; if (sysExData != null) { sysExData.CopyTo(newSysExData, 0); } Array.Copy(data, (int)pos, newSysExData, oldLength, (int)length); sysExData = newSysExData; // Make it a system message if necessary (i.e. if we find an end marker) if (data[pos + length - 1] == 0xF7) { tempEvent = new SystemExclusiveMidiEvent(deltaTime, sysExData); sysExData = null; sysExContinue = false; } } // Nothing we know about else { var e = new MidiParserException("Invalid status byte found.", pos); e.Data["status"] = status; throw e; } // Add the newly parsed event if we got one if (tempEvent != null) { track.Events.Add(tempEvent); } } // Return the newly populated track return(track); } // Let MidiParserExceptions through catch (MidiParserException) { throw; } // Wrap all other exceptions in MidiParserExceptions catch (Exception exc) { throw new MidiParserException("Failed to parse MIDI file.", exc, pos); } }
/// <summary>Parse a meta MIDI event from the data stream.</summary> /// <param name="track">The track that owns this event.</param> /// <param name="deltaTime">The previously parsed delta-time for this event.</param> /// <param name="eventType">The previously parsed type of message we're expecting to find.</param> /// <param name="data">The data stream from which to read the event information.</param> /// <param name="pos">The position of the start of the event information.</param> /// <returns>The parsed meta MIDI event.</returns> private static MidiEvent ParseMetaEvent(MidiTrack track, long deltaTime, byte eventType, byte[] data, ref long pos) { try { MidiEvent tempEvent = null; // Create the correct meta event based on its meta event id/type switch (eventType) { // Sequence number case SequenceNumberMetaMidiEvent.MetaId: pos++; // skip past the 0x02 int number = ((data[pos] << 8) | data[pos + 1]); tempEvent = new SequenceNumberMetaMidiEvent(track, deltaTime, number); pos += 2; // skip read values break; // Text events (copyright, lyrics, etc) case TextMetaMidiEvent.MetaId: tempEvent = new TextMetaMidiEvent(track, deltaTime, ReadASCIIText(data, ref pos)); break; case CopyrightTextMetaMidiEvent.MetaId: tempEvent = new CopyrightTextMetaMidiEvent(track, deltaTime, ReadASCIIText(data, ref pos)); break; case SequenceTrackNameTextMetaMidiEvent.MetaId: tempEvent = new SequenceTrackNameTextMetaMidiEvent(track, deltaTime, ReadASCIIText(data, ref pos)); break; case InstrumentTextMetaMidiEvent.MetaId: tempEvent = new InstrumentTextMetaMidiEvent(track, deltaTime, ReadASCIIText(data, ref pos)); break; case LyricTextMetaMidiEvent.MetaId: tempEvent = new LyricTextMetaMidiEvent(track, deltaTime, ReadASCIIText(data, ref pos)); break; case MarkerTextMetaMidiEvent.MetaId: tempEvent = new MarkerTextMetaMidiEvent(track, deltaTime, ReadASCIIText(data, ref pos)); break; case CuePointTextMetaMidiEvent.MetaId: tempEvent = new CuePointTextMetaMidiEvent(track, deltaTime, ReadASCIIText(data, ref pos)); break; case ProgramNameTextMetaMidiEvent.MetaId: tempEvent = new ProgramNameTextMetaMidiEvent(track, deltaTime, ReadASCIIText(data, ref pos)); break; case DeviceNameTextMidiEvent.MetaId: tempEvent = new DeviceNameTextMidiEvent(track, deltaTime, ReadASCIIText(data, ref pos)); break; // Channel prefix case ChannelPrefixMetaMidiEvent.MetaId: pos++; // skip 0x1 tempEvent = new ChannelPrefixMetaMidiEvent(track, deltaTime, data[pos]); pos++; // skip read value break; // Port number case MidiPortMetaMidiEvent.MetaId: pos++; // skip 0x1 tempEvent = new MidiPortMetaMidiEvent(track, deltaTime, data[pos]); pos++; // skip read value break; // End of track case EndOfTrackMetaMidiEvent.MetaId: pos++; // skip 0x0 tempEvent = new EndOfTrackMetaMidiEvent(track, deltaTime); break; // Tempo case TempoMetaMidiEvent.MetaId: pos++; // skip 0x3 int tempo = ((data[pos] << 16) | data[pos + 1] << 8 | data[pos + 2]); tempEvent = new TempoMetaMidiEvent(track, deltaTime, tempo); pos += 3; break; // SMPTE offset case SMPTEOffsetMetaMidiEvent.MetaId: pos++; // skip 0x5 tempEvent = new SMPTEOffsetMetaMidiEvent(track, deltaTime, data[pos], data[pos + 1], data[pos + 2], data[pos + 3], data[pos + 4]); pos += 5; break; // Time signature case TimeSignatureMetaMidiEvent.MetaId: pos++; // skip past 0x4 tempEvent = new TimeSignatureMetaMidiEvent(track, deltaTime, data[pos], data[pos + 1], data[pos + 2], data[pos + 3]); pos += 4; break; // Key signature case KeySignatureMetaMidiEvent.MetaId: pos++; // skip past 0x2 tempEvent = new KeySignatureMetaMidiEvent(track, deltaTime, (Key)data[pos], (Tonality)data[pos + 1]); pos += 2; break; // Proprietary case ProprietaryMetaMidiEvent.MetaId: // Read in the variable length and that much data, then store it long length = ReadVariableLength(data, ref pos); byte[] propData = new byte[length]; Array.Copy(data, (int)pos, propData, 0, (int)length); tempEvent = new ProprietaryMetaMidiEvent(track, deltaTime, propData); pos += length; break; // An unknown meta event! default: // Read in the variable length and that much data, then store it length = ReadVariableLength(data, ref pos); byte[] unknownData = new byte[length]; Array.Copy(data, (int)pos, unknownData, 0, (int)length); tempEvent = new UnknownMetaMidiEvent(track, deltaTime, eventType, unknownData); pos += length; break; } return(tempEvent); } // Something bad happened; wrap it in a parser exception catch (Exception exc) { throw new MidiParserException("Unable to parse meta MIDI event.", exc, pos); } }
/// <summary>Parse a voice event from the data stream.</summary> /// <param name="track">The track that owns this event.</param> /// <param name="deltaTime">The previously parsed delta-time for this event.</param> /// <param name="messageType">The previously parsed type of message we're expecting to find.</param> /// <param name="channel">The previously parsed channel for this message.</param> /// <param name="data">The data stream from which to read the event information.</param> /// <param name="pos">The position of the start of the event information.</param> /// <returns>The parsed voice MIDI event.</returns> private static MidiEvent ParseVoiceEvent(MidiTrack track, long deltaTime, byte messageType, byte channel, byte[] data, ref long pos) { try { MidiEvent tempEvent = null; // Create the correct voice event based on its message id/type switch (messageType) { // NOTE OFF case OffNoteVoiceMidiEvent.CategoryId: tempEvent = new OffNoteVoiceMidiEvent(track, deltaTime, channel, data[pos], data[pos + 1]); track.Channel = channel; pos += 2; break; // NOTE ON case OnNoteVoiceMidiEvent.CategoryId: tempEvent = new OnNoteVoiceMidiEvent(track, deltaTime, channel, data[pos], data[pos + 1]); track.Channel = channel; pos += 2; break; // AFTERTOUCH case AftertouchNoteVoiceMidiEvent.CategoryId: tempEvent = new AftertouchNoteVoiceMidiEvent(track, deltaTime, channel, data[pos], data[pos + 1]); track.Channel = channel; pos += 2; break; // CONTROLLER case ControllerVoiceMidiEvent.CategoryId: tempEvent = new ControllerVoiceMidiEvent(track, deltaTime, channel, data[pos], data[pos + 1]); pos += 2; break; // PROGRAM CHANGE case ProgramChangeVoiceMidiEvent.CategoryId: tempEvent = new ProgramChangeVoiceMidiEvent(track, deltaTime, channel, data[pos]); pos += 1; break; // CHANNEL PRESSURE case ChannelPressureVoiceMidiEvent.CategoryId: tempEvent = new ChannelPressureVoiceMidiEvent(track, deltaTime, channel, data[pos]); pos += 1; break; // PITCH WHEEL case PitchWheelVoiceMidiEvent.CategoryId: int position = ((data[pos] << 8) | data[pos + 1]); byte upper, lower; MidiEvent.Split14BitsToBytes(position, out upper, out lower); tempEvent = new PitchWheelVoiceMidiEvent(track, deltaTime, channel, upper, lower); pos += 2; break; // UH OH! default: Validate.ThrowOutOfRange("messageType", messageType, 0x8, 0xE); break; } // Return the newly parsed event return(tempEvent); } // Something bad happened; wrap it in a parser exception catch (Exception exc) { throw new MidiParserException("Unable to parse voice MIDI event.", exc, pos); } }
/// <summary>Converts the MIDI sequence into a new one with the desired format.</summary> /// <param name="sequence">The sequence to be converted.</param> /// <param name="format">The format to which we want to convert the sequence.</param> /// <param name="options">Options used when doing the conversion.</param> /// <returns>The new, converted sequence.</returns> public static MidiSequence Convert(this MidiSequence sequence, Format format, FormatConversionOption options) { Validate.NonNull("sequence", sequence); Validate.InRange("format", (int)format, (int)Format.Zero, (int)Format.Two); if (sequence.Format == format) { // If the desired format is the same as the original, just return a copy. // No transformation is necessary. sequence = new MidiSequence(sequence); } else if (format != Format.Zero || sequence.Tracks.Count == 1) { // If the desired format is is not 0 or there's only one track, just copy the sequence with a different format number. // If it's not zero, then multiple tracks are acceptable, so no transformation is necessary. // Or if there's only one track, then there's no possible transformation to be done. var newSequence = new MidiSequence(format, sequence.Division); foreach (MidiTrack t in sequence) { newSequence.Tracks.Add(new MidiTrack(t)); } sequence = newSequence; } else { // Now the harder cases, converting to format 0. We need to combine all tracks into 1, // as format 0 requires that there only be a single track with all of the events for the song. sequence = new MidiSequence(sequence); // Iterate through all events in all tracks and change deltaTimes to actual times. // We'll then be able to sort based on time and change them back to deltas later. foreach (MidiTrack track in sequence) { track.Events.ConvertDeltasToTotals(); } // Add all events to new track (except for end of track markers!) int trackNumber = 0; MidiTrack newTrack = new MidiTrack(); foreach (MidiTrack track in sequence) { foreach (MidiEvent midiEvent in track.Events) { // If this event has a channel, and if we're storing tracks as channels, copy to it if ((options & FormatConversionOption.CopyTrackToChannel) > 0 && trackNumber >= 0 && trackNumber <= 0xF) { var vme = midiEvent as VoiceMidiEvent; if (vme != null) { vme.Channel = (byte)trackNumber; } } // Add all events, except for end of track markers (we'll add our own) if (!(midiEvent is EndOfTrackMetaMidiEvent)) { newTrack.Events.Add(midiEvent); } } trackNumber++; } // Sort the events by total time, then convert back to delta time, // and top things off with an end-of-track marker. newTrack.Events.Sort((x, y) => x.DeltaTime.CompareTo(y.DeltaTime)); newTrack.Events.ConvertTotalsToDeltas(); newTrack.Events.Add(new EndOfTrackMetaMidiEvent(0)); // We now have all of the combined events in newTrack. Clear out the sequence, replacing all the tracks // with this new one. sequence.Tracks.Clear(); sequence.Tracks.Add(newTrack); } return(sequence); }