Mercurial > audlegacy-plugins
view src/amidi-plug/i_midi.c @ 2306:dd78327f5747
keep track of how long song is played
author | Tomasz Mon <desowin@gmail.com> |
---|---|
date | Sun, 13 Jan 2008 17:33:47 +0100 |
parents | 5f892afeb8e1 |
children |
line wrap: on
line source
/* * * Author: Giacomo Lozito <james@develia.org>, (C) 2005-2006 * * MIDI (SMF) parser based on aplaymidi.c from ALSA-utils * aplaymidi.c is Copyright (c) 2004 Clemens Ladisch <clemens@ladisch.de> * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ #include "i_midi.h" #include "i_configure.h" #define ERRMSG_MIDITRACK() { g_warning( "%s: invalid MIDI data (offset %#x)" , mf->file_name , mf->file_offset ); return 0; } /* skip a certain number of bytes */ void i_midi_file_skip_bytes( midifile_t * mf , gint bytes ) { while (bytes > 0) { i_midi_file_read_byte(mf); --bytes; } } /* reads a single byte */ gint i_midi_file_read_byte( midifile_t * mf ) { ++mf->file_offset; return VFS_GETC(mf->file_pointer); } /* reads a little-endian 32-bit integer */ gint i_midi_file_read_32_le( midifile_t * mf ) { gint value; value = i_midi_file_read_byte(mf); value |= i_midi_file_read_byte(mf) << 8; value |= i_midi_file_read_byte(mf) << 16; value |= i_midi_file_read_byte(mf) << 24; return !VFS_FEOF(mf->file_pointer) ? value : -1; } /* reads a 4-character identifier */ gint i_midi_file_read_id( midifile_t * mf ) { return i_midi_file_read_32_le(mf); } /* reads a fixed-size big-endian number */ gint i_midi_file_read_int( midifile_t * mf , gint bytes ) { gint c, value = 0; do { c = i_midi_file_read_byte(mf); if (c == EOF) return -1; value = (value << 8) | c; } while (--bytes); return value; } /* reads a variable-length number */ gint i_midi_file_read_var( midifile_t * mf ) { gint value, c; c = i_midi_file_read_byte(mf); value = c & 0x7f; if (c & 0x80) { c = i_midi_file_read_byte(mf); value = (value << 7) | (c & 0x7f); if (c & 0x80) { c = i_midi_file_read_byte(mf); value = (value << 7) | (c & 0x7f); if (c & 0x80) { c = i_midi_file_read_byte(mf); value = (value << 7) | c; if (c & 0x80) return -1; } } } return value; } /* allocates a new event */ midievent_t * i_midi_file_new_event(midifile_track_t * track, gint sysex_length) { midievent_t * event; event = malloc(sizeof(midievent_t) + sysex_length); /* check_mem(event); */ event->next = NULL; /* append at the end of the track's linked list */ if (track->current_event) track->current_event->next = event; else track->first_event = event; track->current_event = event; return event; } /* reads one complete track from the file */ gint i_midi_file_read_track( midifile_t * mf , midifile_track_t * track , gint track_end , gint port_count ) { gint tick = 0; guchar last_cmd = 0; guchar port = 0; /* the current file position is after the track ID and length */ while ( mf->file_offset < track_end ) { guchar cmd; midievent_t *event; gint delta_ticks, len, c; delta_ticks = i_midi_file_read_var(mf); if ( delta_ticks < 0 ) break; tick += delta_ticks; c = i_midi_file_read_byte(mf); if (c < 0) break; if (c & 0x80) { /* have command */ cmd = c; if (cmd < 0xf0) last_cmd = cmd; } else { /* running status */ VFS_UNGETC(c, mf->file_pointer); mf->file_offset--; cmd = last_cmd; if (!cmd) ERRMSG_MIDITRACK(); } switch (cmd >> 4) { /* maps SMF events to ALSA sequencer events */ static guchar cmd_type[] = { [0x8] = SND_SEQ_EVENT_NOTEOFF, [0x9] = SND_SEQ_EVENT_NOTEON, [0xa] = SND_SEQ_EVENT_KEYPRESS, [0xb] = SND_SEQ_EVENT_CONTROLLER, [0xc] = SND_SEQ_EVENT_PGMCHANGE, [0xd] = SND_SEQ_EVENT_CHANPRESS, [0xe] = SND_SEQ_EVENT_PITCHBEND }; case 0x8: /* channel msg with 2 parameter bytes */ case 0x9: case 0xa: { event = i_midi_file_new_event(track, 0); event->type = cmd_type[cmd >> 4]; event->port = port; event->tick = tick; event->data.d[0] = cmd & 0x0f; /* if this note is not in standard drum channel (10), apply transpose */ if ( event->data.d[0] != 9 ) { gint data_tr = (i_midi_file_read_byte(mf) & 0x7f) + amidiplug_cfg_ap.ap_opts_transpose_value; if ( data_tr > 127 ) data_tr = 127; else if ( data_tr < 0 ) data_tr = 0; event->data.d[1] = (guchar)data_tr; } else /* this note is in standard drum channel (10), apply drum shift */ { gint data_ds = (i_midi_file_read_byte(mf) & 0x7f) + amidiplug_cfg_ap.ap_opts_drumshift_value; /* always > 0 */ if ( data_ds > 127 ) data_ds -= 127; event->data.d[1] = (guchar)data_ds; } event->data.d[2] = i_midi_file_read_byte(mf) & 0x7f; } break; case 0xb: /* channel msg with 2 parameter bytes */ case 0xe: { event = i_midi_file_new_event(track, 0); event->type = cmd_type[cmd >> 4]; event->port = port; event->tick = tick; event->data.d[0] = cmd & 0x0f; event->data.d[1] = i_midi_file_read_byte(mf) & 0x7f; event->data.d[2] = i_midi_file_read_byte(mf) & 0x7f; } break; case 0xc: /* channel msg with 1 parameter byte */ case 0xd: { event = i_midi_file_new_event(track, 0); event->type = cmd_type[cmd >> 4]; event->port = port; event->tick = tick; event->data.d[0] = cmd & 0x0f; event->data.d[1] = i_midi_file_read_byte(mf) & 0x7f; } break; case 0xf: { switch (cmd) { case 0xf0: /* sysex */ case 0xf7: /* continued sysex, or escaped commands */ { len = i_midi_file_read_var(mf); if (len < 0) ERRMSG_MIDITRACK(); if (cmd == 0xf0) ++len; event = i_midi_file_new_event(track, len); event->type = SND_SEQ_EVENT_SYSEX; event->port = port; event->tick = tick; event->data.length = len; if (cmd == 0xf0) { event->sysex[0] = 0xf0; c = 1; } else { c = 0; } for (; c < len; ++c) event->sysex[c] = i_midi_file_read_byte(mf); } break; case 0xff: /* meta event */ { c = i_midi_file_read_byte(mf); len = i_midi_file_read_var(mf); if (len < 0) ERRMSG_MIDITRACK(); switch (c) { case 0x21: /* port number */ { if (len < 1) ERRMSG_MIDITRACK(); port = i_midi_file_read_byte(mf) % port_count; i_midi_file_skip_bytes(mf,(len - 1)); } break; case 0x2f: /* end of track */ { track->end_tick = tick; i_midi_file_skip_bytes(mf,(track_end - mf->file_offset)); return 1; } case 0x51: /* tempo */ { if (len < 3) ERRMSG_MIDITRACK(); if (mf->smpte_timing) { /* SMPTE timing doesn't change */ i_midi_file_skip_bytes(mf,len); } else { event = i_midi_file_new_event(track, 0); event->type = SND_SEQ_EVENT_TEMPO; event->port = port; event->tick = tick; event->data.tempo = i_midi_file_read_byte(mf) << 16; event->data.tempo |= i_midi_file_read_byte(mf) << 8; event->data.tempo |= i_midi_file_read_byte(mf); i_midi_file_skip_bytes(mf,(len - 3)); } } break; case 0x01: /* text comments */ { if ( amidiplug_cfg_ap.ap_opts_comments_extract > 0 ) { gint ic = 0; if (len < 1) ERRMSG_MIDITRACK(); event = i_midi_file_new_event(track, 0); event->type = SND_SEQ_EVENT_META_TEXT; event->tick = tick; event->data.metat = calloc( len + 1 , sizeof(gchar) ); for ( ic = 0 ; ic < len ; ic++ ) event->data.metat[ic] = i_midi_file_read_byte(mf); event->data.metat[len] = '\0'; } else i_midi_file_skip_bytes(mf,len); } break; case 0x05: /* lyrics */ { if ( amidiplug_cfg_ap.ap_opts_lyrics_extract > 0 ) { gint ic = 0; if (len < 1) ERRMSG_MIDITRACK(); event = i_midi_file_new_event(track, 0); event->type = SND_SEQ_EVENT_META_LYRIC; event->tick = tick; event->data.metat = calloc( len + 1 , sizeof(gchar) ); for ( ic = 0 ; ic < len ; ic++ ) event->data.metat[ic] = i_midi_file_read_byte(mf); event->data.metat[len] = '\0'; } else i_midi_file_skip_bytes(mf,len); } break; default: /* ignore all other meta events */ { i_midi_file_skip_bytes(mf,len); } break; } } break; default: /* invalid Fx command */ ERRMSG_MIDITRACK(); } } break; default: /* cannot happen */ ERRMSG_MIDITRACK(); } } ERRMSG_MIDITRACK(); } /* read a MIDI file in Standard MIDI Format */ /* return values: 0 = error , 1 = ok */ gint i_midi_file_parse_smf( midifile_t * mf , gint port_count ) { gint header_len, i; /* the curren position is immediately after the "MThd" id */ header_len = i_midi_file_read_int(mf,4); if ( header_len < 6 ) { g_warning( "%s: invalid file format\n" , mf->file_name ); return 0; } mf->format = i_midi_file_read_int(mf,2); if (( mf->format != 0 ) && ( mf->format != 1 )) { g_warning( "%s: type %d format is not supported\n" , mf->file_name , mf->format); return 0; } mf->num_tracks = i_midi_file_read_int(mf,2); if (( mf->num_tracks < 1 ) || ( mf->num_tracks > 1000 )) { g_warning( "%s: invalid number of tracks (%d)\n" , mf->file_name , mf->num_tracks ); mf->num_tracks = 0; return 0; } mf->tracks = calloc( mf->num_tracks , sizeof(midifile_track_t) ); if ( !mf->tracks ) { g_warning( "out of memory\n" ); mf->num_tracks = 0; return 0; } mf->time_division = i_midi_file_read_int(mf,2); if ( mf->time_division < 0 ) { g_warning( "%s: invalid file format\n" , mf->file_name ); return 0; } mf->smpte_timing = !!(mf->time_division & 0x8000); /* read tracks */ for ( i = 0 ; i < mf->num_tracks ; ++i ) { gint len; /* search for MTrk chunk */ for (;;) { gint id = i_midi_file_read_id(mf); len = i_midi_file_read_int(mf,4); if ( VFS_FEOF(mf->file_pointer) ) { g_warning( "%s: unexpected end of file\n" , mf->file_name ); return 0; } if (( len < 0 ) || ( len >= 0x10000000)) { g_warning( "%s: invalid chunk length %d\n" , mf->file_name , len ); return 0; } if ( id == MAKE_ID('M', 'T', 'r', 'k') ) break; i_midi_file_skip_bytes(mf,len); } if ( !i_midi_file_read_track( mf , &mf->tracks[i] , mf->file_offset + len , port_count ) ) return 0; } /* calculate the max_tick for the entire file */ mf->max_tick = 0; for ( i = 0 ; i < mf->num_tracks ; ++i ) { if ( mf->tracks[i].end_tick > mf->max_tick ) mf->max_tick = mf->tracks[i].end_tick; } /* ok, success */ return 1; } /* read a MIDI file enclosed in RIFF format */ /* return values: 0 = error , 1 = ok */ gint i_midi_file_parse_riff( midifile_t * mf ) { /* skip file length (4 bytes) */ i_midi_file_skip_bytes(mf,4); /* check file type ("RMID" = RIFF MIDI) */ if ( i_midi_file_read_id(mf) != MAKE_ID('R', 'M', 'I', 'D') ) return 0; /* search for "data" chunk */ for (;;) { gint id = i_midi_file_read_id(mf); gint len = i_midi_file_read_32_le(mf); if ( VFS_FEOF(mf->file_pointer) ) return 0; if ( id == MAKE_ID('d', 'a', 't', 'a') ) break; if (len < 0) return 0; i_midi_file_skip_bytes(mf,((len + 1) & ~1)); } /* the "data" chunk must contain data in SMF format */ if ( i_midi_file_read_id(mf) != MAKE_ID('M', 'T', 'h', 'd') ) return 0; /* ok, success */ return 1; } /* midifile init */ void i_midi_init( midifile_t * mf ) { mf->file_pointer = NULL; mf->file_name = NULL; mf->file_offset = 0; mf->num_tracks = 0; mf->tracks = NULL; mf->max_tick = 0; mf->smpte_timing = 0; mf->format = 0; mf->time_division = 0; mf->ppq = 0; mf->current_tempo = 0; mf->playing_tick = 0; mf->seeking_tick = -1; mf->avg_microsec_per_tick = 0; mf->length = 0; mf->skip_offset = 0; return; } void i_midi_free( midifile_t * mf ) { if ( mf->tracks ) { gint i; /* free event list for each track */ for ( i = 0 ; i < mf->num_tracks ; ++i ) { midievent_t * event = mf->tracks[i].first_event; midievent_t * event_tmp = NULL; while( event ) { event_tmp = event; event = event->next; if (( event_tmp->type == SND_SEQ_EVENT_META_TEXT ) || ( event_tmp->type == SND_SEQ_EVENT_META_LYRIC )) free( event_tmp->data.metat ); free( event_tmp ); } } /* free track array */ free( mf->tracks ); mf->tracks = NULL; } } /* queue set tempo */ gint i_midi_setget_tempo( midifile_t * mf ) { gint smpte_timing , i = 0; gint time_division = mf->time_division; /* interpret and set tempo */ smpte_timing = !!(time_division & 0x8000); if (!smpte_timing) { /* time_division is ticks per quarter */ mf->current_tempo = 500000; mf->ppq = time_division; } else { /* upper byte is negative frames per second */ i = 0x80 - ((time_division >> 8) & 0x7f); /* lower byte is ticks per frame */ time_division &= 0xff; /* now pretend that we have quarter-note based timing */ switch (i) { case 24: mf->current_tempo = 500000; mf->ppq = 12 * time_division; break; case 25: mf->current_tempo = 400000; mf->ppq = 10 * time_division; break; case 29: /* 30 drop-frame */ mf->current_tempo = 100000000; mf->ppq = 2997 * time_division; break; case 30: mf->current_tempo = 500000; mf->ppq = 15 * time_division; break; default: g_warning("Invalid number of SMPTE frames per second (%d)\n", i); return 0; } } DEBUGMSG( "MIDI tempo set -> time division: %i\n" , midifile.time_division ); DEBUGMSG( "MIDI tempo set -> tempo: %i\n" , midifile.current_tempo ); DEBUGMSG( "MIDI tempo set -> ppq: %i\n" , midifile.ppq ); return 1; } /* this will set the midi length in microseconds COMMENT: this will also reset current position in each track! */ void i_midi_setget_length( midifile_t * mf ) { gint length_microsec = 0, last_tick = 0, i = 0; /* get the first microsec_per_tick ratio */ gint microsec_per_tick = (gint)(mf->current_tempo / mf->ppq); /* initialize current position in each track */ for (i = 0; i < mf->num_tracks; ++i) mf->tracks[i].current_event = mf->tracks[i].first_event; /* search for tempo events in each track; in fact, since the program currently supports type 0 and type 1 MIDI files, we should find tempo events only in one track */ DEBUGMSG( "LENGTH calc: starting calc loop\n" ); for (;;) { midievent_t * event = NULL; midifile_track_t * event_track = NULL; gint i, min_tick = mf->max_tick + 1; /* search next event */ for ( i = 0 ; i < mf->num_tracks ; ++i ) { midifile_track_t * track = &mf->tracks[i]; midievent_t * e2 = track->current_event; if (e2 && e2->tick < min_tick) { min_tick = e2->tick; event = e2; event_track = track; } } if (!event) { /* calculate the remaining length */ length_microsec += ( microsec_per_tick * ( mf->max_tick - last_tick ) ); break; /* end of song reached */ } /* advance pointer to next event */ event_track->current_event = event->next; /* check if this is a tempo event */ if ( event->type == SND_SEQ_EVENT_TEMPO ) { DEBUGMSG( "LENGTH calc: tempo event (%i) encountered during calc on tick %i\n" , event->data.tempo , event->tick ); /* increment length_microsec with the amount of microsec before tempo change */ length_microsec += ( microsec_per_tick * ( event->tick - last_tick ) ); /* now update last_tick and the microsec_per_tick ratio */ last_tick = event->tick; microsec_per_tick = (gint)(event->data.tempo / mf->ppq); } } /* IMPORTANT this couple of important values is set by i_midi_set_length */ mf->length = length_microsec; mf->avg_microsec_per_tick = (gint)(length_microsec / mf->max_tick); return; } /* this will get the weighted average bpm of the midi file; if the file has a variable bpm, 'bpm' is set to -1; COMMENT: this will also reset current position in each track! */ void i_midi_get_bpm( midifile_t * mf , gint * bpm , gint * wavg_bpm ) { gint i = 0 , last_tick = 0; guint weighted_avg_tempo = 0; gboolean is_monotempo = TRUE; gint last_tempo = mf->current_tempo; /* initialize current position in each track */ for ( i = 0 ; i < mf->num_tracks ; ++i ) mf->tracks[i].current_event = mf->tracks[i].first_event; /* search for tempo events in each track; in fact, since the program currently supports type 0 and type 1 MIDI files, we should find tempo events only in one track */ DEBUGMSG( "BPM calc: starting calc loop\n" ); for (;;) { midievent_t * event = NULL; midifile_track_t * event_track = NULL; gint i, min_tick = mf->max_tick + 1; /* search next event */ for ( i = 0 ; i < mf->num_tracks ; ++i ) { midifile_track_t * track = &mf->tracks[i]; midievent_t * e2 = track->current_event; if (e2 && e2->tick < min_tick) { min_tick = e2->tick; event = e2; event_track = track; } } if (!event) { /* calculate the remaining length */ weighted_avg_tempo += (guint)( last_tempo * ((gfloat)( mf->max_tick - last_tick ) / (gfloat)mf->max_tick ) ); break; /* end of song reached */ } /* advance pointer to next event */ event_track->current_event = event->next; /* check if this is a tempo event */ if ( event->type == SND_SEQ_EVENT_TEMPO ) { /* check if this is a tempo change (real change, tempo should be different) in the midi file (and it shouldn't be at tick 0); */ if (( is_monotempo ) && ( event->tick > 0 ) && ( event->data.tempo != last_tempo )) is_monotempo = FALSE; DEBUGMSG( "BPM calc: tempo event (%i) encountered during calc on tick %i\n" , event->data.tempo , event->tick ); /* add the previous tempo change multiplied for its weight (the tick interval for the tempo ) */ weighted_avg_tempo += (guint)( last_tempo * ((gfloat)( event->tick - last_tick ) / (gfloat)mf->max_tick ) ); /* now update last_tick and the microsec_per_tick ratio */ last_tick = event->tick; last_tempo = event->data.tempo; } } DEBUGMSG( "BPM calc: weighted average tempo: %i\n" , weighted_avg_tempo ); *wavg_bpm = (gint)( 60000000 / weighted_avg_tempo ); DEBUGMSG( "BPM calc: weighted average bpm: %i\n" , *wavg_bpm ); if ( is_monotempo ) *bpm = *wavg_bpm; /* the song has fixed bpm */ else *bpm = -1; /* the song has variable bpm */ return; } /* helper function that parses a midi file; returns 1 on success, 0 otherwise */ gint i_midi_parse_from_filename( gchar * filename , midifile_t * mf ) { i_midi_init( mf ); DEBUGMSG( "PARSE_FROM_FILENAME requested, opening file: %s\n" , filename ); mf->file_pointer = VFS_FOPEN( filename , "rb" ); if (!mf->file_pointer) { g_warning( "Cannot open %s\n" , filename ); return 0; } mf->file_name = filename; switch( i_midi_file_read_id( mf ) ) { case MAKE_ID('R', 'I', 'F', 'F'): { DEBUGMSG( "PARSE_FROM_FILENAME requested, RIFF chunk found, processing...\n" ); /* read riff chunk */ if ( !i_midi_file_parse_riff( mf ) ) WARNANDBREAK( "%s: invalid file format (riff parser)\n" , filename ); /* if that was read correctly, go ahead and read smf data */ } case MAKE_ID('M', 'T', 'h', 'd'): { DEBUGMSG( "PARSE_FROM_FILENAME requested, MThd chunk found, processing...\n" ); /* we don't care about port count here, pass 1 */ if ( !i_midi_file_parse_smf( mf , 1 ) ) WARNANDBREAK( "%s: invalid file format (smf parser)\n" , filename ); if ( mf->time_division < 1 ) WARNANDBREAK( "%s: invalid time division (%i)\n" , filename , mf->time_division ); /* fill mf->ppq and mf->tempo using time_division */ if ( !i_midi_setget_tempo( mf ) ) WARNANDBREAK( "%s: invalid values while setting ppq and tempo\n" , filename ); /* fill mf->length, keeping in count tempo-changes */ i_midi_setget_length( mf ); /* ok, mf has been filled with information; successfully return */ VFS_FCLOSE( mf->file_pointer ); return 1; } default: { g_warning( "%s is not a Standard MIDI File\n" , filename ); break; } } /* something failed */ VFS_FCLOSE( mf->file_pointer ); return 0; }