// allegrosmfwr.cpp -- Allegro Standard Midi File Write #include #include #include #include #include #include #include using namespace std; #include "allegro.h" // event_queue is a list element that keeps track of pending // things to write to a track, including note-ons, note-offs, // updates, tempo changes, and time signatures // class event_queue{ public: char type;//'n' for note, 'o' for off, 's' for time signature, // 'c' for tempo changes double time; long index; //of the event in mSeq->notes class event_queue *next; event_queue(char t, double when, long x, class event_queue *n) { type = t; time = when; index = x; next = n; } }; class Alg_smf_write { public: Alg_smf_write(Alg_seq_ptr seq); ~Alg_smf_write(); long channels_per_track; // used to encode track number into chan field // chan is actual_channel + channels_per_track * track_number // default is 100, set this to 0 to merge all tracks to 16 channels void write(ofstream &file /* , midiFileFormat = 1 */); private: long previous_divs; // time in ticks of most recently written event void write_track(int i); void write_tempo(int divs, int tempo); void write_tempo_change(int i); void write_time_signature(int i); void write_note(Alg_note_ptr note, bool on); void write_update(Alg_update_ptr update); void write_text(Alg_update_ptr update, char type); void write_binary(int type_byte, char *msg); void write_midi_channel_prefix(Alg_update_ptr update); void write_smpteoffset(Alg_update_ptr update, char *s); void write_data(int data); int to_midi_channel(int channel); int to_track(int channel); ostream *out_file; Alg_seq_ptr seq; int num_tracks; // number of tracks not counting tempo track int division; // divisions per quarter note, default = 120 int initial_tempo; int timesig_num; // numerator of time signature int timesig_den; // denominator of time signature double timesig_when; // time of time signature int keysig; // number of sharps (+) or flats (-), -99 for undefined char keysig_mode; // 'M' or 'm' for major/minor double keysig_when; // time of key signature void write_delta(double event_time); void write_varinum(int num); void write_16bit(int num); void write_24bit(int num); void write_32bit(int num); }; #define ROUND(x) (int) ((x)+0.5) Alg_smf_write::Alg_smf_write(Alg_seq_ptr a_seq) { out_file = NULL; // at 100bpm (a nominal tempo value), we would like a division // to represent 1ms of time. So // d ticks/beat * 100 beats/min = 60,000 ms/min * 1 tick/ms // solving for d, d = 600 division = 600; // divisions per quarter note timesig_num = timesig_den = 0; // initially undefined keysig = -99; keysig_mode = 0; initial_tempo = 500000; seq = a_seq; previous_divs = 0; // used to compute deltas for midifile } Alg_smf_write::~Alg_smf_write() { } // sorting is quite subtle due to rounding // For example, suppose times from a MIDI file are exact, but in // decimal round to TW0.4167 Q0.3333. Since the time in whole notes // rounded up, this note will start late. Even though the duration // rounded down, the amount is 1/4 as much because units are quarter // notes. Therefore, the total roundup is 0.0001 beats. This is // enough to cause the note to sort later in the queue, perhaps // coming after a new note-on on the same pitch, and resulting in // a turning on-off, on-off into on, on, off, off if data is moved // to Allegro (ascii) format with rounding and then back to SMF. // // The solution here is to consider things that round to the same // tick to be simultaneous. Then, be sure to deal with note-offs // before note-ons. We're going to do that by using event_queue // times that are rounded to the nearest tick time. Except note-offs // are going to go in with times that are 1/4 tick earlier so they // get scheduled first, but still end up on the same tick. // event_queue* push(event_queue *queue, event_queue *event) { // printf("push: %.6g, %c, %d\n", event->time, event->type, event->index); if (queue == NULL) { event->next = NULL; return event; } event_queue *marker1 = NULL; event_queue *marker2 = queue; while (marker2 != NULL && marker2->time <= event->time) { marker1 = marker2; marker2 = marker2->next; } event->next = marker2; if (marker1 != NULL) { marker1->next=event; return queue; } else return event; } void print_queue(event_queue *q) { printf("Printing queue. . .\n"); event_queue *q2=q; while (q2) { printf("%c at %f ;", q2->type, q2->time); q2 = q2->next; } printf("\nDone printing.\n"); } void Alg_smf_write::write_note(Alg_note_ptr note, bool on) { double event_time = (on ? note->time : note->time + note->dur); write_delta(event_time); //printf("deltaDivisions: %d, beats elapsed: %g, on? %c\n", deltaDivisions, note->time, on); char chan = (note->chan & 15); int pitch = int(note->pitch + 0.5); if (pitch < 0) { pitch = pitch % 12; } else if (pitch > 127) { pitch = (pitch % 12) + 120; // put pitch in 10th octave if (pitch > 127) pitch -= 12; // or 9th octave } out_file->put(0x90 + chan); out_file->put(pitch); if (on) { int vel = (int) note->loud; if (vel <= 0) vel = 1; write_data(vel); } else out_file->put(0); // note-off indicated by velocty zero } void Alg_smf_write::write_midi_channel_prefix(Alg_update_ptr update) { if (update->chan >= 0) { // write MIDI Channel Prefix write_delta(update->time); out_file->put(0xFF); // Meta Event out_file->put(0x20); // Type code for MIDI Channel Prefix out_file->put(1); // length out_file->put(to_midi_channel(update->chan)); // one thing odd about the Std MIDI File spec is that once // you turn on MIDI Channel Prefix, there seems to be no // way to cancel it unless a non-Meta event shows up. We // don't do any analysis to avoid assigning channels to // meta events. } } void Alg_smf_write::write_text(Alg_update_ptr update, char type) { write_midi_channel_prefix(update); write_delta(update->time); out_file->put(0xFF); out_file->put(type); out_file->put((char) strlen(update->parameter.s)); *out_file << update->parameter.s; } void Alg_smf_write::write_smpteoffset(Alg_update_ptr update, char *s) { write_midi_channel_prefix(update); write_delta(update->time); out_file->put(0xFF); // meta event out_file->put(0x54); // smpte offset type code out_file->put(5); // length for (int i = 0; i < 5; i++) *out_file << s[i]; } // write_data - limit data to the range of [0...127] and write it void Alg_smf_write::write_data(int data) { if (data < 0) data = 0; else if (data > 0x7F) data = 0x7F; out_file->put(data); } int Alg_smf_write::to_midi_channel(int channel) { // allegro track number is stored as multiple of 100 // also mask off all but 4 channel bits just in case if (channels_per_track > 0) channel %= channels_per_track; return channel & 0xF; } int Alg_smf_write::to_track(int channel) { if (channel == -1) return 0; return channel / channels_per_track; } static char hex_to_nibble(char c) { if (isalpha(c)) { return 10 + (toupper(c) - 'A'); } else { return c - '0'; } } static char hex_to_char(char *s) { return (hex_to_nibble(s[0]) << 4) + hex_to_nibble(s[1]); } void Alg_smf_write::write_binary(int type_byte, char *msg) { int len = strlen(msg) / 2; out_file->put(type_byte); write_varinum(len); for (int i = 0; i < len; i++) { out_file->put(hex_to_char(msg)); msg += 2; } } void Alg_smf_write::write_update(Alg_update_ptr update) { char *name = update->parameter.attr_name(); /****Non-Meta Events****/ if (!strcmp(name, "pressurer")) { write_delta(update->time); if (update->get_identifier() < 0) { // channel pressure message out_file->put(0xD0 + to_midi_channel(update->chan)); write_data((int)(update->parameter.r * 127)); } else { // just 1 key -- poly pressure out_file->put(0xA0 + to_midi_channel(update->chan)); write_data(update->get_identifier()); write_data((int)(update->parameter.r * 127)); } } else if (!strcmp(name, "programi")) { write_delta(update->time); out_file->put(0xC0 + to_midi_channel(update->chan)); write_data(update->parameter.i); } else if (!strcmp(name, "bendr")) { int temp = ROUND(0x2000 * (update->parameter.r + 1)); if (temp > 0x3fff) temp = 0x3fff; // 14 bits maximum if (temp < 0) temp = 0; int c1 = temp & 0x7F; // low 7 bits int c2 = temp >> 7; // high 7 bits write_delta(update->time); out_file->put(0xE0 + to_midi_channel(update->chan)); write_data(c1); write_data(c2); } else if (!strncmp(name, "control", 7) && update->parameter.attr_type() == 'r') { int ctrlnum = atoi(name + 7); int val = ROUND(update->parameter.r * 127); write_delta(update->time); out_file->put(0xB0 + to_midi_channel(update->chan)); write_data(ctrlnum); write_data(val); } else if (!strcmp(name, "sysexs") && update->parameter.attr_type() == 's') { char *s = update->parameter.s; if (s[0] && s[1] && toupper(s[0]) == 'F' && s[1] == '0') { s += 2; // skip the initial "F0" byte in message: it is implied } write_delta(update->time); write_binary(0xF0, s); } else if (!strcmp(name, "sqspecifics") && update->parameter.attr_type() == 's') { char *s = update->parameter.s; write_delta(update->time); out_file->put(0xFF); write_binary(0x7F, s); /****Text Events****/ } else if (!strcmp(name, "texts")) { write_text(update, 0x01); } else if (!strcmp(name, "copyrights")) { write_text(update, 0x02); } else if (!strcmp(name, "seqnames") || !strcmp(name, "tracknames")) { write_text(update, 0x03); } else if (!strcmp(name, "instruments")) { write_text(update, 0x04); } else if (!strcmp(name, "lyrics")) { write_text(update, 0x05); } else if (!strcmp(name, "markers")) { write_text(update, 0x06); } else if (!strcmp(name, "cues")) { write_text(update, 0x07); } else if (!strcmp(name, "miscs")) { write_text(update, 0x08); /****Other Events****/ } else if (!strcmp(name, "smpteoffsets")) { #define decimal(p) (((p)[0] - '0') * 10 + ((p)[1] - '0')) // smpteoffset is specified as "24fps:00h:10m:00s:11.00f" // the following simple parser does not reject all badly // formatted strings, but it should parse good strings ok char *s = update->parameter.s; int len = strlen(s); char smpteoffset[5]; if (len < 24) return; // not long enough, must be bad format int fps = 0; if (s[0] == '2') { if (s[1] == '4') fps = 0; else if (s[1] == '5') fps = 1; else if (s[1] == '9') { fps = 2; if (len != 27) return; // not right length s += 3; // cancel effect of longer string } } else fps = 3; s += 6; int hours = decimal(s); s += 4; int mins = decimal(s); s += 4; int secs = decimal(s); s += 4; int frames = decimal(s); s += 3; int subframes = decimal(s); smpteoffset[0] = (fps << 6) + hours; smpteoffset[1] = mins; smpteoffset[2] = secs; smpteoffset[3] = frames; smpteoffset[4] = subframes; write_smpteoffset(update, smpteoffset); // key signature is special because it takes two events in the Alg_seq // structure to make one midi file event. When we encounter one or // the other event, we'll just record it in the Alg_smf_write object. // After both events are seen, we write the data. (See below.) } else if (!strcmp(name, "keysigi")) { keysig = update->parameter.i; keysig_when = update->time; } else if (!strcmp(name, "modea")) { if (!strcmp(alg_attr_name(update->parameter.a), "major")) keysig_mode = 'M'; else keysig_mode = 'm'; keysig_when = update->time; } if (keysig != -99 && keysig_mode) { // write when both are defined write_delta(keysig_when); out_file->put(0xFF); out_file->put(0x59); out_file->put(2); // mask off high bits so that this value appears to be positive // i.e. -1 -> 0xFF (otherwise, write_data will clip -1 to 0) out_file->put(keysig & 0xFF); out_file->put(keysig_mode == 'm'); keysig = -99; keysig_mode = false; } //printf("Update: %s, key: %g\n", update->parameter.attr_name(), update->key); } // see notes on event_queue::push, TICK_TIME converts from beat to // the number of the nearest tick. The second parameter is an offset in // quarter ticks. By scheduling with -1, note-offs should get dispatched // first. Note that TICK_TIME only determines the order of events, so // it is ok to change units from beats to ticks, saving a divide. #define TICK_TIME(t, o) (ROUND((t) * division) + 0.25 * (o)) void Alg_smf_write::write_track(int i) { int j = 0; // note index Alg_events ¬es = seq->track_list[i]; event_queue *pending = NULL; if (notes.length() > 0) { pending = new event_queue('n', TICK_TIME(notes[j]->time, 0), 0, NULL); } if (i == 0) { // track 0 may have tempo and timesig info if (seq->get_time_map()->last_tempo_flag || seq->get_time_map()->beats.len > 0) { pending = push(pending, new event_queue('c', 0.0, 0, NULL)); } if (seq->time_sig.length() > 0) { pending = push(pending, new event_queue('s', TICK_TIME(seq->time_sig[0].beat, 0), 0, NULL)); } } while (pending) { event_queue *current = pending; pending = pending->next; if (current->type == 'n') { Alg_note_ptr n = (Alg_note_ptr) notes[current->index]; if (n->is_note()) { write_note(n, true); pending = push(pending, new event_queue('o', TICK_TIME(n->time + n->dur, -1), current->index, NULL)); } else if (n->is_update()) { Alg_update_ptr u = (Alg_update_ptr) n; write_update(u); } int next = current->index + 1; if (next < notes.length()) { current->time = TICK_TIME(notes[next]->time, 0); current->index = next; pending = push(pending, current); } } else if (current->type == 'o') { //note-off Alg_note_ptr n = (Alg_note_ptr) notes[current->index]; write_note(n, false); delete current; } else if (current->type == 'c') { // tempo change write_tempo_change(current->index); current->index++; // -R if (current->index < seq->get_time_map()->beats.len) { current->time = TICK_TIME(seq->get_time_map()-> beats[current->index].beat, 0); pending = push(pending, current); } else { delete current; } } else if (current->type == 's') { // time sig write_time_signature(current->index); current->index++; if (current->index < seq->time_sig.length()) { current->time = TICK_TIME(seq->time_sig[current->index].beat, 0); pending = push(pending, current); } else { delete current; } } } } void Alg_smf_write::write_tempo(int divs, int tempo) { // printf("Inserting tempo %f after %f clocks.\n", tempo, delta); write_varinum(divs - previous_divs); previous_divs = divs; out_file->put(0xFF); out_file->put(0x51); out_file->put(0x03); write_24bit((int)tempo); } void Alg_smf_write::write_tempo_change(int i) // i is index of tempo map { // extract tempo map Alg_beats &b = seq->get_time_map()->beats; double tempo; long divs; if (i < seq->get_time_map()->beats.len - 1) { tempo = 1000000 * ((b[i+1].time - b[i].time) / (b[i+1].beat - b[i].beat)); divs = ROUND(b[i].beat * division); write_tempo(divs, ROUND(tempo)); } else if (seq->get_time_map()->last_tempo_flag) { // write the final tempo divs = ROUND(division * b[i].beat); tempo = (1000000.0 / seq->get_time_map()->last_tempo); write_tempo(divs, ROUND(tempo)); } } void Alg_smf_write::write_time_signature(int i) { Alg_time_sigs &ts = seq->time_sig; // write the time signature long divs = ROUND(ts[i].beat * division); write_varinum(divs - previous_divs); out_file->put(0xFF); out_file->put(0x58); // time signature out_file->put(4); // length of message out_file->put(ROUND(ts[i].num)); int den = ROUND(ts[i].den); int den_byte = 0; while (den > 1) { // compute the log2 of denominator den_byte++; den >>= 1; } out_file->put(den_byte); out_file->put(24); // clocks per quarter out_file->put(8); // 32nd notes per 24 clocks } void Alg_smf_write::write(ofstream &file) { int track_len_offset; int track_end_offset; int track_len; out_file = &file; // Header file << "MThd"; write_32bit(6); // chunk length write_16bit(1); // format 1 MIDI file write_16bit(seq->tracks()); // number of tracks write_16bit(division); // divisions per quarter note // write_ all tracks seq->convert_to_beats(); int i; for (i = 0; i < seq->tracks(); i++) { previous_divs = 0; *out_file << "MTrk"; track_len_offset = out_file->tellp(); write_32bit(0); // track len placeholder write_track(i); // End of track event write_varinum(0); // delta time out_file->put(0xFF); out_file->put(0x2F); out_file->put(0x00); // Go back and write in the length of the track track_end_offset = out_file->tellp(); track_len = track_end_offset - track_len_offset - 4; out_file->seekp(track_len_offset); write_32bit(track_len); out_file->seekp(track_end_offset); } } void Alg_smf_write::write_16bit(int num) { out_file->put((num & 0xFF00) >> 8); out_file->put(num & 0xFF); } void Alg_smf_write::write_24bit(int num) { out_file->put((num & 0xFF0000) >> 16); out_file->put((num & 0xFF00) >> 8); out_file->put((num & 0xFF)); } void Alg_smf_write::write_32bit(int num) { out_file->put((num & 0xFF000000) >> 24); out_file->put((num & 0xFF0000) >> 16); out_file->put((num & 0xFF00) >> 8); out_file->put((num & 0xFF)); } void Alg_smf_write::write_delta(double event_time) { // divisions is ideal absolute time in divisions long divisions = ROUND(division * event_time); long delta_divs = divisions - previous_divs; write_varinum(delta_divs); previous_divs = divisions; } void Alg_smf_write::write_varinum(int value) { if(value<0) value=0;//this line should not have to be here! int buffer; buffer = value & 0x7f; while ((value >>= 7) > 0) { buffer <<= 8; buffer |= 0x80; buffer += (value & 0x7f); } for(;;) { out_file->put(buffer); if (buffer & 0x80) buffer >>= 8; else break; } } void Alg_seq::smf_write(ofstream &file) { Alg_smf_write writer(this); writer.write(file); } bool Alg_seq::smf_write(const char *filename) { ofstream outf(filename, ios::binary | ios::out); if (outf.fail()) return false; smf_write(outf); outf.close(); return true; }