diff --git a/src/midi.rs b/src/midi.rs
index 078d5ca..fe4d0b9 100644
--- a/src/midi.rs
+++ b/src/midi.rs
@@ -1,7 +1,8 @@
 #[derive(PartialEq, Eq, Debug, Clone)]
 pub enum Message {
     Voice(VoiceMessage),
-    System(SystemMessage),
+    System(SystemCommon),
+    Realtime(SystemRealtime),
 }
 
 #[derive(PartialEq, Eq, Debug, Clone)]
@@ -28,4 +29,23 @@ pub enum VoiceCategory {
 }
 
 #[derive(PartialEq, Eq, Debug, Clone)]
-pub enum SystemMessage {}
+pub enum SystemCommon {
+    SystemExclusive { data: Vec<u8> },
+    MidiTimeCode { time_code: u8 },
+    SongPositionPointer { value: u16 },
+    SongSelect { song_number: u8 },
+    TuneRequest,
+    Unknown,
+}
+
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub enum SystemRealtime {
+    Clock,
+    Tick,
+    Start,
+    Stop,
+    Continue,
+    ActiveSense,
+    Reset,
+    Unknown,
+}
diff --git a/src/parser.rs b/src/parser.rs
index a04d9f8..43b7c20 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -1,25 +1,65 @@
-use nom::{bytes::complete::take, IResult};
+use nom::{
+    bytes::complete::{tag, take, take_till},
+    combinator::opt,
+    IResult,
+};
 
-use crate::midi::{Message, SystemMessage, VoiceCategory, VoiceMessage};
+use crate::midi::{Message, SystemCommon, SystemRealtime, VoiceCategory, VoiceMessage};
 
 pub fn parse_message(bytes: &[u8]) -> IResult<&[u8], Message> {
     let (bytes, status_byte) = take(1usize)(bytes)?;
     let status_byte = status_byte[0];
 
+    // TODO: implement running status; see [1].
+    // [1]: http://midi.teragonaudio.com/tech/midispec/run.htm
+
     if status_byte < 0xF0 {
         let (bytes, vm) = parse_voice_message(status_byte, bytes)?;
         Ok((bytes, Message::Voice(vm)))
+    } else if status_byte < 0xf8 {
+        let (bytes, sc) = parse_system_common(status_byte, bytes)?;
+        Ok((bytes, Message::System(sc)))
     } else {
-        let (bytes, sm) = parse_status_message(status_byte, bytes)?;
-        Ok((bytes, Message::System(sm)))
+        let sr = parse_system_realtime(status_byte);
+        Ok((bytes, Message::Realtime(sr)))
     }
 }
 
-fn parse_status_message(_status_byte: u8, bytes: &[u8]) -> IResult<&[u8], SystemMessage> {
-    Err(nom::Err::Error(nom::error::Error {
-        input: bytes,
-        code: nom::error::ErrorKind::Fail,
-    }))
+/// The highest order bit is only ever set for status bytes, so we can test
+/// for to tell whether or not we've found a status byte. This is needed for
+/// finding the end of dynamic-length messages, such as SysEx, which is
+/// considered aborted if another status byte comes before 0xf7.
+fn is_status_byte(b: u8) -> bool {
+    (b & 0x80) != 0
+}
+
+fn parse_system_common(status_byte: u8, bytes: &[u8]) -> IResult<&[u8], SystemCommon> {
+    match status_byte {
+        0xf0 => parse_system_exclusive(bytes),
+        0xf1 => one_byte_message(bytes, |time_code| SystemCommon::MidiTimeCode { time_code }),
+        0xf2 => parse_song_position_pointer(bytes),
+        0xf3 => one_byte_message(bytes, |song_number| SystemCommon::SongSelect {
+            song_number,
+        }),
+        0xf6 => Ok((bytes, SystemCommon::TuneRequest)),
+        _ => Err(nom::Err::Error(nom::error::Error {
+            input: bytes,
+            code: nom::error::ErrorKind::Fail,
+        })),
+    }
+}
+
+pub fn parse_system_realtime(status_byte: u8) -> SystemRealtime {
+    match status_byte {
+        0xf8 => SystemRealtime::Clock,
+        0xf9 => SystemRealtime::Tick,
+        0xfa => SystemRealtime::Start,
+        0xfb => SystemRealtime::Continue,
+        0xfc => SystemRealtime::Stop,
+        0xfe => SystemRealtime::ActiveSense,
+        0xff => SystemRealtime::Reset,
+        _ => SystemRealtime::Unknown,
+    }
 }
 
 pub fn parse_voice_message(status_byte: u8, remainder: &[u8]) -> IResult<&[u8], VoiceMessage> {
@@ -30,10 +70,17 @@ pub fn parse_voice_message(status_byte: u8, remainder: &[u8]) -> IResult<&[u8],
     let (remainder, category) = match category_nibble {
         0x80 => parse_voice_note(remainder, true)?,
         0x90 => parse_voice_note(remainder, false)?,
-        0xa0 => parse_aftertouch(remainder)?,
-        0xb0 => parse_control_change(remainder)?,
-        0xc0 => parse_program_change(remainder)?,
-        0xd0 => parse_channel_pressure(remainder)?,
+        0xa0 => two_byte_message(remainder, |note, pressure| VoiceCategory::AfterTouch {
+            note,
+            pressure,
+        })?,
+        0xb0 => two_byte_message(remainder, |controller, value| {
+            VoiceCategory::ControlChange { controller, value }
+        })?,
+        0xc0 => one_byte_message(remainder, |value| VoiceCategory::ProgramChange { value })?,
+        0xd0 => one_byte_message(remainder, |pressure| VoiceCategory::ChannelPressure {
+            pressure,
+        })?,
         0xe0 => parse_pitch_wheel(remainder)?,
         _ => {
             return Err(nom::Err::Error(nom::error::Error {
@@ -47,81 +94,55 @@ pub fn parse_voice_message(status_byte: u8, remainder: &[u8]) -> IResult<&[u8],
 }
 
 pub fn parse_voice_note(bytes: &[u8], off: bool) -> IResult<&[u8], VoiceCategory> {
-    let (remainder, data) = take(2usize)(bytes)?;
-
-    let note = data[0];
-    let velocity = data[1];
-
-    let category = if velocity == 0 || off {
-        VoiceCategory::NoteOff { note, velocity }
-    } else {
-        VoiceCategory::NoteOn { note, velocity }
-    };
-
-    Ok((remainder, category))
-}
-
-pub fn generic_error(bytes: &[u8]) -> nom::Err<nom::error::Error<&[u8]>> {
-    nom::Err::Error(nom::error::Error {
-        input: bytes,
-        code: nom::error::ErrorKind::Fail,
+    two_byte_message(bytes, |note, velocity| {
+        if velocity == 0 || off {
+            VoiceCategory::NoteOff { note, velocity }
+        } else {
+            VoiceCategory::NoteOn { note, velocity }
+        }
     })
 }
 
 pub fn parse_pitch_wheel(bytes: &[u8]) -> IResult<&[u8], VoiceCategory> {
-    if bytes.len() < 2 {
-        return Err(generic_error(bytes));
-    }
-
-    let (db1, db2) = (bytes[0], bytes[1]);
-    let value = ((db1 as u16) << 7) | db2 as u16;
-
-    Ok((&bytes[2..], VoiceCategory::PitchWheel { value }))
+    let (bytes, value) = take_14_bit_value(bytes)?;
+    Ok((bytes, VoiceCategory::PitchWheel { value }))
 }
 
-pub fn parse_control_change(bytes: &[u8]) -> IResult<&[u8], VoiceCategory> {
-    if bytes.len() < 2 {
-        return Err(generic_error(bytes));
-    }
+pub fn parse_system_exclusive(bytes: &[u8]) -> IResult<&[u8], SystemCommon> {
+    let (remainder, data) = take_till(is_status_byte)(bytes)?;
+    let (remainder, _) = opt(tag([0xf7]))(remainder)?;
 
-    let controller = bytes[0];
-    let value = bytes[1];
+    let data: Vec<u8> = data.into();
 
-    Ok((
-        &bytes[2..],
-        VoiceCategory::ControlChange { controller, value },
-    ))
+    Ok((remainder, SystemCommon::SystemExclusive { data }))
 }
 
-pub fn parse_program_change(bytes: &[u8]) -> IResult<&[u8], VoiceCategory> {
-    if bytes.is_empty() {
-        return Err(generic_error(bytes));
-    }
-
-    let value = bytes[0];
-
-    Ok((&bytes[1..], VoiceCategory::ProgramChange { value }))
+pub fn parse_song_position_pointer(bytes: &[u8]) -> IResult<&[u8], SystemCommon> {
+    let (remainder, value) = take_14_bit_value(bytes)?;
+    Ok((remainder, SystemCommon::SongPositionPointer { value }))
 }
 
-pub fn parse_aftertouch(bytes: &[u8]) -> IResult<&[u8], VoiceCategory> {
-    if bytes.len() < 2 {
-        return Err(generic_error(bytes));
-    }
-
-    let note = bytes[0];
-    let pressure = bytes[1];
-
-    Ok((&bytes[2..], VoiceCategory::AfterTouch { note, pressure }))
+pub fn one_byte_message<T, F>(bytes: &[u8], f: F) -> IResult<&[u8], T>
+where
+    F: Fn(u8) -> T,
+{
+    let (bytes, b) = take(1usize)(bytes)?;
+    Ok((bytes, f(b[0])))
 }
 
-pub fn parse_channel_pressure(bytes: &[u8]) -> IResult<&[u8], VoiceCategory> {
-    if bytes.is_empty() {
-        return Err(generic_error(bytes));
-    }
+pub fn two_byte_message<T, F>(bytes: &[u8], f: F) -> IResult<&[u8], T>
+where
+    F: Fn(u8, u8) -> T,
+{
+    let (bytes, b) = take(2usize)(bytes)?;
+    Ok((bytes, f(b[0], b[1])))
+}
 
-    let pressure = bytes[0];
+pub fn take_14_bit_value(bytes: &[u8]) -> IResult<&[u8], u16> {
+    let (bytes, db) = take(2usize)(bytes)?;
+    let value = ((db[0] as u16) << 7) | db[1] as u16;
 
-    Ok((&bytes[1..], VoiceCategory::ChannelPressure { pressure }))
+    Ok((bytes, value))
 }
 
 #[cfg(test)]