/* * Copyright (C) 2015 Stas Sergeev * * The below copyright strings have to be distributed unchanged together * with this file. This prefix can not be modified or separated. */ /* * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ /* * Purpose: midi file writer * * Author: Stas Sergeev * * This code is a mix of fluidsynth output plugin and the file output * plugin from timidity. The timidity file output plugin, in turn, is * a port of dosemu's midid file output code by Rob Komar and Stas Sergeev. * So this code made a really long way. */ #include #include #include #include #include #include #include #include "fluid_midi.h" #include "emu.h" #include "init.h" #include "timers.h" #include "sound/midi.h" #define midofile_name "MIDI Output: midi file" static fluid_midi_parser_t* parser; static int output_running; static long long mf_time_base; #define IGNORE_TEMPO_EVENTS 0 static char *midibuf; static size_t midi_pos; static FILE *fp; static long track_size_pos; static double last_time; static int tempo; static int ticks_per_quarter_note; #define TICKS_OFFSET 12 #define BE_SHORT htons #define BE_LONG htonl static size_t m_fwrite(const void *ptr, size_t size); #define M_FWRITE_STR(s) m_fwrite(s, sizeof(s) - 1) #define M_FWRITE1(a) do { \ uint8_t __arg[1]; \ __arg[0] = a; \ m_fwrite(__arg, 1); \ } while (0) #define M_FWRITE2(a, b) do { \ uint8_t __arg[2]; \ __arg[0] = a; \ __arg[1] = b; \ m_fwrite(__arg, 2); \ } while (0) #define M_FWRITE3(a, b, c) do { \ uint8_t __arg[3]; \ __arg[0] = a; \ __arg[1] = b; \ __arg[2] = c; \ m_fwrite(__arg, 3); \ } while (0) static size_t m_fwrite(const void *ptr, size_t size) { return fwrite(ptr, size, 1, fp); } static void write_midi_header(void) { /* Write out MID file header. * The file will have a single track, with the configured number of * ticks per quarter note. */ M_FWRITE_STR("MThd"); M_FWRITE_STR("\0\0\0\6"); /* header size */ M_FWRITE_STR("\0\0"); /* single track format */ M_FWRITE_STR("\0\1"); /* #tracks = 1 */ M_FWRITE_STR("\0\0"); /* #ticks / quarter note written later */ } static void finalize_midi_header(void) { uint16_t tpqn = BE_SHORT(ticks_per_quarter_note); fflush(fp); memcpy(midibuf + TICKS_OFFSET, &tpqn, 2); /* #ticks / quarter note */ } static void set_tempo(void) { M_FWRITE_STR("\xff\x51\3"); M_FWRITE3(tempo >> 16, tempo >> 8, tempo); } static void set_time_sig(void) { /* Set the time sig to 4/4 */ M_FWRITE_STR("\xff\x58\4\4\x2\x18\x08"); } static void midout_write_delta_time(int32_t time) { int32_t delta_time; unsigned char c[4]; int idx; int started_printing = 0; #if !IGNORE_TEMPO_EVENTS double div; delta_time = time - last_time; div = tempo / (double)ticks_per_quarter_note; delta_time /= div; last_time += delta_time * div; #else delta_time = time - last_time; last_time += delta_time; #endif /* We have to divide the number of ticks into 7-bit segments, and only write * the non-zero segments starting with the most significant (except for the * least significant segment, which we always write). The most significant bit * is set to 1 in all but the least significant segment. */ c[0] = (delta_time >> 21) & 0x7f; c[1] = (delta_time >> 14) & 0x7f; c[2] = (delta_time >> 7) & 0x7f; c[3] = (delta_time) & 0x7f; for (idx = 0; idx < 3; idx++) { if (started_printing || c[idx]) { started_printing = 1; M_FWRITE1(c[idx] | 0x80); } } M_FWRITE1(c[3]); } static void start_midi_track(void) { /* Write out track header. * The track will have a large length (0x7fffffff) because we don't know at * this time how big it will really be. */ M_FWRITE_STR("MTrk"); fflush(fp); track_size_pos = midi_pos; M_FWRITE_STR("\x7f\xff\xff\xff"); /* #chunks */ last_time = 0; #if !IGNORE_TEMPO_EVENTS tempo = 500000; #else tempo = ticks_per_quarter_note; #endif midout_write_delta_time(0); set_tempo(); midout_write_delta_time(0); set_time_sig(); } static void end_midi_track(void) { int32_t track_bytes; /* Send (with delta-time of 0) "0xff 0x2f 0x0" to finish the track. */ M_FWRITE_STR("\0\xff\x2f\0"); fflush(fp); track_bytes = BE_LONG(midi_pos - track_size_pos - 4); memcpy(midibuf + track_size_pos, &track_bytes, 4); } static int open_output(void) { fp = open_memstream(&midibuf, &midi_pos); ticks_per_quarter_note = 144; write_midi_header(); return 0; } static void close_output(void) { int fd; finalize_midi_header(); fclose(fp); fd = open(config.midi_file, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd == -1) { error("MIDI: failed to open %s: %s\n", config.midi_file, strerror(errno)); goto out; } write(fd, midibuf, midi_pos); close(fd); out: free(midibuf); } static void midout_noteon(int chn, int note, int vel, int32_t time) { midout_write_delta_time(time); M_FWRITE3((chn & 0x0f) | NOTE_ON, note & 0x7f, vel & 0x7f); } static void midout_noteoff(int chn, int note, int vel, int32_t time) { midout_write_delta_time(time); M_FWRITE3((chn & 0x0f) | NOTE_OFF, note & 0x7f, vel & 0x7f); } static void midout_control(int chn, int control, int value, int32_t time) { midout_write_delta_time(time); M_FWRITE3((chn & 0x0f) | CONTROL_CHANGE, control & 0x7f, value & 0x7f); } static void midout_keypressure(int chn, int control, int value, int32_t time) { midout_write_delta_time(time); M_FWRITE3((chn & 0x0f) | KEY_PRESSURE, control & 0x7f, value & 0x7f); } static void midout_channelpressure(int chn, int vel, int32_t time) { midout_write_delta_time(time); M_FWRITE2((chn & 0x0f) | CHANNEL_PRESSURE, vel & 0x7f); } static void midout_bender(int chn, int pitch, int32_t time) { midout_write_delta_time(time); M_FWRITE3((chn & 0x0f) | PITCH_BEND, pitch & 0x7f, (pitch >> 7) & 0x7f); } static void midout_program(int chn, int pgm, int32_t time) { midout_write_delta_time(time); M_FWRITE2((chn & 0x0f) | PROGRAM_CHANGE, pgm & 0x7f); } static void midout_tempo(int chn, int a, int b, int32_t time) { midout_write_delta_time(time); tempo = (a << 16) | (b << 8) | chn; set_tempo(); } #if 0 static int find_bit(int val) { int i = 0; while (val) { if (val & 1) return i; i++; val >>= 1; } return -1; } static void midout_timesig(int chn, int a, int b, int32_t time) { if (chn == 0) { if (!b) return; b = find_bit(b); midout_write_delta_time(time); M_FWRITE_STR("\xff\x58\4"); } M_FWRITE2(a, b); } #endif static void midout_sysex(void *data, int len, int32_t time) { uint8_t a; int l1 = len + 1; midout_write_delta_time(time); M_FWRITE1(0xf0); a = l1 & 0x7f; if (l1 != a) { uint8_t b = ((l1 >> 7) & 0x7f) | 0x80; M_FWRITE1(b); } M_FWRITE1(a); m_fwrite(data, len); M_FWRITE1(0xf7); } static void do_event(fluid_midi_event_t *ev, int32_t time) { int ch = fluid_midi_event_get_channel(ev); switch (fluid_midi_event_get_type(ev)) { case NOTE_ON: midout_noteon(ch, fluid_midi_event_get_key(ev), fluid_midi_event_get_velocity(ev), time); break; case NOTE_OFF: midout_noteoff(ch, fluid_midi_event_get_key(ev), fluid_midi_event_get_velocity(ev), time); break; case KEY_PRESSURE: midout_keypressure(ch, fluid_midi_event_get_key(ev), fluid_midi_event_get_value(ev), time); break; case PROGRAM_CHANGE: midout_program(ch, fluid_midi_event_get_program(ev), time); break; case CHANNEL_PRESSURE: midout_channelpressure(ch, fluid_midi_event_get_value(ev), time); break; case PITCH_BEND: midout_bender(ch, fluid_midi_event_get_pitch(ev), time); break; case CONTROL_CHANGE: midout_control(ch, fluid_midi_event_get_control(ev), fluid_midi_event_get_value(ev), time); break; case MIDI_TIME_CODE: #if !IGNORE_TEMPO_EVENTS midout_tempo(ch, fluid_midi_event_get_key(ev), fluid_midi_event_get_value(ev), time); #endif break; #if 0 case ME_TIMESIG: midout_timesig(ch, ev->a, ev->b, time); break; #endif case MIDI_SYSEX: midout_sysex(ev->paramptr, ev->param1, time); break; } } static int midofile_init(void *arg) { parser = new_fluid_midi_parser(); return 1; } static void midofile_done(void *arg) { delete_fluid_midi_parser(parser); } static void midofile_start(void) { S_printf("MIDI: starting fluidsynth\n"); mf_time_base = GETusTIME(0); open_output(); output_running = 1; start_midi_track(); } static void midofile_write(unsigned char val) { fluid_midi_event_t* event; if (!output_running) midofile_start(); event = fluid_midi_parser_parse(parser, val); if (event != NULL) { unsigned long long now = GETusTIME(0); int32_t usec = now - mf_time_base; if (debug_level('S') >= 5) S_printf("MIDI: sending event to fluidsynth, usec=%i\n", usec); do_event(event, usec); } } static void midofile_stop(void *arg) { if (!output_running) return; end_midi_track(); close_output(); output_running = 0; } static int midofile_get_cfg(void *arg) { if (config.midi_file && config.midi_file[0]) return PCM_CF_ENABLED; return 0; } static const struct midi_out_plugin midofile #ifdef __cplusplus { midofile_name, NULL, midofile_get_cfg, midofile_init, midofile_done, 0, midofile_write, midofile_stop, NULL, ST_ANY, PCM_F_PASSTHRU | PCM_F_EXPLICIT, }; #else = { .name = midofile_name, .get_cfg = midofile_get_cfg, .open = midofile_init, .close = midofile_done, .write = midofile_write, .stop = midofile_stop, .stype = ST_ANY, .flags = PCM_F_PASSTHRU | PCM_F_EXPLICIT, }; #endif CONSTRUCTOR(static void midofile_register(void)) { midi_register_output_plugin(&midofile); }