From eb1d6b3a4e16fdc8ef5cdce62f0dd193de44c128 Mon Sep 17 00:00:00 2001 From: trasz Date: Sat, 11 Oct 2008 22:37:31 +0000 Subject: [PATCH] Import of jack-smf-utils 1.0. git-svn-id: https://jack-smf-utils.svn.sourceforge.net/svnroot/jack-smf-utils/trunk@2 1236be98-edfb-4384-941c-bd23afbee59c --- AUTHORS | 2 + COPYING | 24 ++ Makefile.am | 2 + TODO | 4 + configure.ac | 86 ++++ libsmf/COPYING | 24 ++ libsmf/Makefile.am | 9 + libsmf/TODO | 10 + libsmf/smf.c | 1055 +++++++++++++++++++++++++++++++++++++++++++++++ libsmf/smf.h | 343 +++++++++++++++ libsmf/smf_decode.c | 624 ++++++++++++++++++++++++++++ libsmf/smf_load.c | 890 +++++++++++++++++++++++++++++++++++++++ libsmf/smf_private.h | 60 +++ libsmf/smf_save.c | 523 +++++++++++++++++++++++ libsmf/smf_tempo.c | 421 +++++++++++++++++++ libsmf/smfsh.c | 879 +++++++++++++++++++++++++++++++++++++++ man/Makefile.am | 4 + man/jack-smf-player.1 | 55 +++ man/jack-smf-recorder.1 | 25 ++ src/Makefile.am | 10 + src/jack-smf-player.c | 771 ++++++++++++++++++++++++++++++++++ src/jack-smf-recorder.c | 498 ++++++++++++++++++++++ 22 files changed, 6319 insertions(+) create mode 100644 AUTHORS create mode 100644 COPYING create mode 100644 Makefile.am create mode 100644 TODO create mode 100644 configure.ac create mode 100644 libsmf/COPYING create mode 100644 libsmf/Makefile.am create mode 100644 libsmf/TODO create mode 100644 libsmf/smf.c create mode 100644 libsmf/smf.h create mode 100644 libsmf/smf_decode.c create mode 100644 libsmf/smf_load.c create mode 100644 libsmf/smf_private.h create mode 100644 libsmf/smf_save.c create mode 100644 libsmf/smf_tempo.c create mode 100644 libsmf/smfsh.c create mode 100644 man/Makefile.am create mode 100644 man/jack-smf-player.1 create mode 100644 man/jack-smf-recorder.1 create mode 100644 src/Makefile.am create mode 100644 src/jack-smf-player.c create mode 100644 src/jack-smf-recorder.c diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..ca7b296 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,2 @@ +Edward Tomasz Napierała + diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..79ec85f --- /dev/null +++ b/COPYING @@ -0,0 +1,24 @@ +Copyright (c) 2007, 2008 Edward Tomasz Napierała +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +ALTHOUGH THIS SOFTWARE IS MADE OF WIN AND SCIENCE, IT IS PROVIDED BY THE +AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..e3c64b1 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,2 @@ +SUBDIRS = libsmf src man + diff --git a/TODO b/TODO new file mode 100644 index 0000000..7e9843f --- /dev/null +++ b/TODO @@ -0,0 +1,4 @@ +- Became a timebase master. + +- Be a good timebase client, adapt to tempo. + diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..c759046 --- /dev/null +++ b/configure.ac @@ -0,0 +1,86 @@ +# -*- Autoconf -*- +# Process this file with autoconf to produce a configure script. + +AC_PREREQ(2.61) +AC_INIT([jack-smf-utils], [1.0], [trasz@FreeBSD.org]) +AM_INIT_AUTOMAKE([-Wall foreign]) +AC_CONFIG_SRCDIR([src/jack-smf-player.c]) +AC_CONFIG_HEADER([config.h]) + +# Checks for programs. +AC_PROG_CC +AC_PROG_INSTALL +AC_PROG_RANLIB + +# Checks for libraries. +AC_CHECK_LIB([m], [pow]) +AC_ARG_WITH([readline], + [AS_HELP_STRING([--with-readline], + [support fancy command line editing @<:@default=check@:>@])], + [], + [with_readline=check]) + +AS_IF([test "x$with_readline" != xno], + [AC_CHECK_LIB([readline], [main], + [AC_SUBST([READLINE_LIBS], ["-lreadline -lncurses"]) + AC_DEFINE([HAVE_LIBREADLINE], [1], [Define if you have libreadline])], + [if test "x$with_readline" != xcheck; then + AC_MSG_FAILURE( + [--with-readline was given, but test for readline failed]) + fi + ], -lncurses)]) + +# Checks for header files. +AC_HEADER_STDC +AC_CHECK_HEADERS([arpa/inet.h stdlib.h string.h sys/time.h unistd.h]) + +# Checks for typedefs, structures, and compiler characteristics. +AC_C_CONST +AC_TYPE_INT8_T +AC_HEADER_TIME +AC_TYPE_UINT16_T +AC_TYPE_UINT32_T +AC_C_VOLATILE + +# Checks for library functions. +AC_FUNC_MALLOC +AC_FUNC_MEMCMP +AC_FUNC_REALLOC +AC_TYPE_SIGNAL +AC_FUNC_STRTOD +AC_CHECK_FUNCS([gettimeofday memset pow strdup strerror strtol]) + +PKG_CHECK_MODULES(GLIB, glib-2.0 >= 2.2) +AC_SUBST(GLIB_CFLAGS) +AC_SUBST(GLIB_LIBS) + +PKG_CHECK_MODULES(GTHREAD, gthread-2.0 >= 2.2) +AC_SUBST(GTHREAD_CFLAGS) +AC_SUBST(GTHREAD_LIBS) + +PKG_CHECK_MODULES(JACK, jack >= 0.102.0) +AC_SUBST(JACK_CFLAGS) +AC_SUBST(JACK_LIBS) + +PKG_CHECK_MODULES(JACK_MIDI_NEEDS_NFRAMES, jack < 0.105.00, + AC_DEFINE(JACK_MIDI_NEEDS_NFRAMES, 1, [whether or not JACK routines need nframes parameter]), true) + +AC_ARG_WITH([lash], + [AS_HELP_STRING([--with-lash], + [support LASH @<:@default=check@:>@])], + [], + [with_lash=check]) + +AS_IF([test "x$with_lash" != xno], + [PKG_CHECK_MODULES(LASH, lash-1.0, AC_DEFINE([HAVE_LASH], [], [Defined if we have LASH support.]), + [if test "x$with_lash" != xcheck; then + AC_MSG_FAILURE([--with-lash was given, but LASH was not found]) + fi + ])]) + +AC_SUBST(LASH_CFLAGS) +AC_SUBST(LASH_LIBS) + +AC_CONFIG_FILES([Makefile libsmf/Makefile man/Makefile src/Makefile]) +AC_OUTPUT + diff --git a/libsmf/COPYING b/libsmf/COPYING new file mode 100644 index 0000000..79ec85f --- /dev/null +++ b/libsmf/COPYING @@ -0,0 +1,24 @@ +Copyright (c) 2007, 2008 Edward Tomasz Napierała +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +ALTHOUGH THIS SOFTWARE IS MADE OF WIN AND SCIENCE, IT IS PROVIDED BY THE +AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/libsmf/Makefile.am b/libsmf/Makefile.am new file mode 100644 index 0000000..eb91b99 --- /dev/null +++ b/libsmf/Makefile.am @@ -0,0 +1,9 @@ +noinst_LIBRARIES = libsmf.a +libsmf_a_SOURCES = smf.h smf_private.h smf.c smf_decode.c smf_load.c smf_save.c smf_tempo.c +libsmf_a_CFLAGS = $(GLIB_CFLAGS) -DG_LOG_DOMAIN=\"libsmf\" + +bin_PROGRAMS = smfsh +smfsh_SOURCES = smfsh.c +smfsh_CFLAGS = $(GLIB_CFLAGS) -DG_LOG_DOMAIN=\"smfsh\" +smfsh_LDADD = libsmf.a $(GLIB_LIBS) $(READLINE_LIBS) + diff --git a/libsmf/TODO b/libsmf/TODO new file mode 100644 index 0000000..8ca22c5 --- /dev/null +++ b/libsmf/TODO @@ -0,0 +1,10 @@ +Things to do: + + - Implement parsing of sysexes chopped into several "packets". + + - Rework removal of tempo changes. Right now there is one scenario that is broken - when + you have two tempo-related events at the end of the song (i.e. there are no events following), + removing one of these tempo-related events will remove both tempo changes. + + - Add a way to force some particular tempo during playback and ignore the following tempo changes. + diff --git a/libsmf/smf.c b/libsmf/smf.c new file mode 100644 index 0000000..efda978 --- /dev/null +++ b/libsmf/smf.c @@ -0,0 +1,1055 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * ALTHOUGH THIS SOFTWARE IS MADE OF WIN AND SCIENCE, IT IS PROVIDED BY THE + * AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +/* + * This is Standard MIDI File format implementation. + * + * For questions and comments, contact Edward Tomasz Napierala . + */ + +/* Reference: http://www.borg.com/~jglatt/tech/midifile.htm */ + +#include +#include +#include +#include +#include +#include +#include "smf.h" +#include "smf_private.h" + +/** + * Allocates new smf_t structure. + * \return pointer to smf_t or NULL. + */ +smf_t * +smf_new(void) +{ + smf_t *smf = malloc(sizeof(smf_t)); + if (smf == NULL) { + g_critical("Cannot allocate smf_t structure: %s", strerror(errno)); + return NULL; + } + + memset(smf, 0, sizeof(smf_t)); + + smf->tracks_array = g_ptr_array_new(); + assert(smf->tracks_array); + + smf->tempo_array = g_ptr_array_new(); + assert(smf->tempo_array); + + smf_set_ppqn(smf, 120); + smf_set_format(smf, 0); + smf_init_tempo(smf); + + return smf; +} + +/** + * Frees smf and all it's descendant structures. + */ +void +smf_delete(smf_t *smf) +{ + /* Remove all the tracks, from last to first. */ + while (smf->tracks_array->len > 0) + smf_track_delete(g_ptr_array_index(smf->tracks_array, smf->tracks_array->len - 1)); + + assert(smf->tracks_array->len == 0); + assert(smf->number_of_tracks == 0); + g_ptr_array_free(smf->tracks_array, TRUE); + g_ptr_array_free(smf->tempo_array, TRUE); + + memset(smf, 0, sizeof(smf_t)); + free(smf); +} + +/** + * Allocates new smf_track_t structure. + * \return pointer to smf_track_t or NULL. + */ +smf_track_t * +smf_track_new(void) +{ + smf_track_t *track = malloc(sizeof(smf_track_t)); + if (track == NULL) { + g_critical("Cannot allocate smf_track_t structure: %s", strerror(errno)); + return NULL; + } + + memset(track, 0, sizeof(smf_track_t)); + track->next_event_number = -1; + + track->events_array = g_ptr_array_new(); + assert(track->events_array); + + return track; +} + +/** + * Detaches track from its smf and frees it. + */ +void +smf_track_delete(smf_track_t *track) +{ + assert(track); + assert(track->events_array); + + /* Remove all the events, from last to first. */ + while (track->events_array->len > 0) + smf_event_delete(g_ptr_array_index(track->events_array, track->events_array->len - 1)); + + if (track->smf) + smf_remove_track(track); + + assert(track->events_array->len == 0); + assert(track->number_of_events == 0); + g_ptr_array_free(track->events_array, TRUE); + + memset(track, 0, sizeof(smf_track_t)); + free(track); +} + + +/** + * Appends smf_track_t to smf. + */ +void +smf_add_track(smf_t *smf, smf_track_t *track) +{ + assert(track->smf == NULL); + + track->smf = smf; + g_ptr_array_add(smf->tracks_array, track); + + smf->number_of_tracks++; + track->track_number = smf->number_of_tracks; + + if (smf->number_of_tracks > 1) + smf_set_format(smf, 1); +} + +/** + * Detaches track from the smf. + */ +void +smf_remove_track(smf_track_t *track) +{ + int i; + smf_track_t *tmp; + + assert(track->smf != NULL); + + track->smf->number_of_tracks--; + + assert(track->smf->tracks_array); + g_ptr_array_remove(track->smf->tracks_array, track); + + /* Renumber the rest of the tracks, so they are consecutively numbered. */ + for (i = track->track_number; i <= track->smf->number_of_tracks; i++) { + tmp = smf_get_track_by_number(track->smf, i); + tmp->track_number = i; + } + + track->track_number = -1; + track->smf = NULL; +} + +/** + * Allocates new smf_event_t structure. The caller is responsible for allocating + * event->midi_buffer, filling it with MIDI data and setting event->midi_buffer_length properly. + * Note that event->midi_buffer will be freed by smf_event_delete. + * \return pointer to smf_event_t or NULL. + */ +smf_event_t * +smf_event_new(void) +{ + smf_event_t *event = malloc(sizeof(smf_event_t)); + if (event == NULL) { + g_critical("Cannot allocate smf_event_t structure: %s", strerror(errno)); + return NULL; + } + + memset(event, 0, sizeof(smf_event_t)); + + event->delta_time_pulses = -1; + event->time_pulses = -1; + event->time_seconds = -1.0; + event->track_number = -1; + + return event; +} + +/** + * Allocates an smf_event_t structure and fills it with "len" bytes copied + * from "midi_data". + * \param midi_data Pointer to MIDI data. It sill be copied to the newly allocated event->midi_buffer. + * \param len Length of the buffer. It must be proper MIDI event length, e.g. 3 for Note On event. + * \return Event containing MIDI data or NULL. + */ +smf_event_t * +smf_event_new_from_pointer(void *midi_data, int len) +{ + smf_event_t *event; + + event = smf_event_new(); + if (event == NULL) + return NULL; + + event->midi_buffer_length = len; + event->midi_buffer = malloc(event->midi_buffer_length); + if (event->midi_buffer == NULL) { + g_critical("Cannot allocate MIDI buffer structure: %s", strerror(errno)); + smf_event_delete(event); + + return NULL; + } + + memcpy(event->midi_buffer, midi_data, len); + + return event; +} + +/** + * Allocates an smf_event_t structure and fills it with at most three bytes of data. + * For example, if you need to create Note On event, do something like this: + * + * smf_event_new_from_bytes(0x90, 0x3C, 0x7f); + * + * To create event for MIDI message that is shorter than three bytes, do something + * like this: + * + * smf_event_new_from_bytes(0xC0, 0x42, -1); + * + * \param first_byte First byte of MIDI message. Must be valid status byte. + * \param second_byte Second byte of MIDI message or -1, if message is one byte long. + * \param third_byte Third byte of MIDI message or -1, if message is two bytes long. + * \return Event containing MIDI data or NULL. + */ +smf_event_t * +smf_event_new_from_bytes(int first_byte, int second_byte, int third_byte) +{ + int len; + + smf_event_t *event; + + event = smf_event_new(); + if (event == NULL) + return NULL; + + if (first_byte < 0) { + g_critical("First byte of MIDI message cannot be < 0"); + smf_event_delete(event); + + return NULL; + } + + if (first_byte > 255) { + g_critical("smf_event_new_from_bytes: first byte is %d, which is larger than 255.", first_byte); + return NULL; + } + + if (!is_status_byte(first_byte)) { + g_critical("smf_event_new_from_bytes: first byte is not a valid status byte."); + return NULL; + } + + + if (second_byte < 0) + len = 1; + else if (third_byte < 0) + len = 2; + else + len = 3; + + if (len > 1) { + if (second_byte > 255) { + g_critical("smf_event_new_from_bytes: second byte is %d, which is larger than 255.", second_byte); + return NULL; + } + + if (is_status_byte(second_byte)) { + g_critical("smf_event_new_from_bytes: second byte cannot be a status byte."); + return NULL; + } + } + + if (len > 2) { + if (third_byte > 255) { + g_critical("smf_event_new_from_bytes: third byte is %d, which is larger than 255.", third_byte); + return NULL; + } + + if (is_status_byte(third_byte)) { + g_critical("smf_event_new_from_bytes: third byte cannot be a status byte."); + return NULL; + } + } + + event->midi_buffer_length = len; + event->midi_buffer = malloc(event->midi_buffer_length); + if (event->midi_buffer == NULL) { + g_critical("Cannot allocate MIDI buffer structure: %s", strerror(errno)); + smf_event_delete(event); + + return NULL; + } + + event->midi_buffer[0] = first_byte; + if (len > 1) + event->midi_buffer[1] = second_byte; + if (len > 2) + event->midi_buffer[2] = third_byte; + + return event; +} + +/** + * Detaches event from its track and frees it. + */ +void +smf_event_delete(smf_event_t *event) +{ + if (event->track != NULL) + smf_track_remove_event(event); + + if (event->midi_buffer != NULL) + free(event->midi_buffer); + + memset(event, 0, sizeof(smf_event_t)); + free(event); +} + +/** + * Used for sorting track->events_array. + */ +static gint +events_array_compare_function(gconstpointer aa, gconstpointer bb) +{ + smf_event_t *a, *b; + + /* "The comparison function for g_ptr_array_sort() doesn't take the pointers + from the array as arguments, it takes pointers to the pointers in the array." */ + a = (smf_event_t *)*(gpointer *)aa; + b = (smf_event_t *)*(gpointer *)bb; + + if (a->time_pulses < b->time_pulses) + return -1; + + if (a->time_pulses > b->time_pulses) + return 1; + + return 0; +} + +/* + * An assumption here is that if there is an EOT event, it will be at the end of the track. + */ +static void +remove_eot_if_before_pulses(smf_track_t *track, int pulses) +{ + smf_event_t *event; + + event = smf_track_get_last_event(track); + + if (event == NULL) + return; + + if (!smf_event_is_eot(event)) + return; + + if (event->time_pulses > pulses) + return; + + smf_track_remove_event(event); +} + +/** + * Adds the event to the track and computes ->delta_pulses. Note that it is faster + * to append events to the end of the track than to insert them in the middle. + * Usually you want to use smf_track_add_event_seconds or smf_track_add_event_pulses + * instead of this one. Event needs to have ->time_pulses and ->time_seconds already set. + * If you try to add event after an EOT, EOT event will be automatically deleted. + */ +void +smf_track_add_event(smf_track_t *track, smf_event_t *event) +{ + int i; + int last_pulses = 0; + + assert(track->smf != NULL); + assert(event->track == NULL); + assert(event->delta_time_pulses == -1); + assert(event->time_pulses >= 0); + assert(event->time_seconds >= 0.0); + + remove_eot_if_before_pulses(track, event->time_pulses); + + event->track = track; + event->track_number = track->track_number; + + if (track->number_of_events == 0) { + assert(track->next_event_number == -1); + track->next_event_number = 1; + } + + if (track->number_of_events > 0) + last_pulses = smf_track_get_last_event(track)->time_pulses; + + track->number_of_events++; + + /* Are we just appending element at the end of the track? */ + if (last_pulses <= event->time_pulses) { + event->delta_time_pulses = event->time_pulses - last_pulses; + assert(event->delta_time_pulses >= 0); + g_ptr_array_add(track->events_array, event); + event->event_number = track->number_of_events; + + /* We need to insert in the middle of the track. XXX: This is slow. */ + } else { + /* Append, then sort according to ->time_pulses. */ + g_ptr_array_add(track->events_array, event); + g_ptr_array_sort(track->events_array, events_array_compare_function); + + /* Renumber entries and fix their ->delta_pulses. */ + for (i = 1; i <= track->number_of_events; i++) { + smf_event_t *tmp = smf_track_get_event_by_number(track, i); + tmp->event_number = i; + + if (tmp->delta_time_pulses != -1) + continue; + + if (i == 1) { + tmp->delta_time_pulses = tmp->time_pulses; + } else { + tmp->delta_time_pulses = tmp->time_pulses - + smf_track_get_event_by_number(track, i - 1)->time_pulses; + assert(tmp->delta_time_pulses >= 0); + } + } + } + + if (smf_event_is_tempo_change_or_time_signature(event)) { + if (smf_event_is_last(event)) + maybe_add_to_tempo_map(event); + else + smf_create_tempo_map_and_compute_seconds(event->track->smf); + } +} + +/** + * Add End Of Track metaevent. Using it is optional, libsmf will automatically + * add EOT to the tracks during smf_save, with delta_pulses 0. If you try to add EOT + * in the middle of the track, it will fail and nonzero value will be returned. + * If you try to add EOT after another EOT event, it will be added, but the existing + * EOT event will be removed. + * + * \return 0 if everything went ok, nonzero otherwise. + */ +int +smf_track_add_eot_delta_pulses(smf_track_t *track, int delta) +{ + smf_event_t *event; + + event = smf_event_new_from_bytes(0xFF, 0x2F, 0x00); + if (event == NULL) + return -1; + + smf_track_add_event_delta_pulses(track, event, delta); + + return 0; +} + +int +smf_track_add_eot_pulses(smf_track_t *track, int pulses) +{ + smf_event_t *event, *last_event; + + last_event = smf_track_get_last_event(track); + if (last_event != NULL) { + if (last_event->time_pulses > pulses) + return -2; + } + + event = smf_event_new_from_bytes(0xFF, 0x2F, 0x00); + if (event == NULL) + return -3; + + smf_track_add_event_pulses(track, event, pulses); + + return 0; +} + +int +smf_track_add_eot_seconds(smf_track_t *track, double seconds) +{ + smf_event_t *event, *last_event; + + last_event = smf_track_get_last_event(track); + if (last_event != NULL) { + if (last_event->time_seconds > seconds) + return -2; + } + + event = smf_event_new_from_bytes(0xFF, 0x2F, 0x00); + if (event == NULL) + return -1; + + smf_track_add_event_seconds(track, event, seconds); + + return 0; +} + +/** + * Detaches event from its track. + */ +void +smf_track_remove_event(smf_event_t *event) +{ + int i, was_last; + smf_event_t *tmp; + smf_track_t *track; + + assert(event->track != NULL); + assert(event->track->smf != NULL); + + track = event->track; + was_last = smf_event_is_last(event); + + /* Adjust ->delta_time_pulses of the next event. */ + if (event->event_number < track->number_of_events) { + tmp = smf_track_get_event_by_number(track, event->event_number + 1); + assert(tmp); + tmp->delta_time_pulses += event->delta_time_pulses; + } + + track->number_of_events--; + g_ptr_array_remove(track->events_array, event); + + if (track->number_of_events == 0) + track->next_event_number = -1; + + /* Renumber the rest of the events, so they are consecutively numbered. */ + for (i = event->event_number; i <= track->number_of_events; i++) { + tmp = smf_track_get_event_by_number(track, i); + tmp->event_number = i; + } + + if (smf_event_is_tempo_change_or_time_signature(event)) { + /* XXX: This will cause problems, when there is more than one Tempo Change event at a given time. */ + if (was_last) + remove_last_tempo_with_pulses(event->track->smf, event->time_pulses); + else + smf_create_tempo_map_and_compute_seconds(track->smf); + } + + event->track = NULL; + event->event_number = -1; + event->delta_time_pulses = -1; + event->time_pulses = -1; + event->time_seconds = -1.0; +} + +/** + * \return Nonzero if event is Tempo Change or Time Signature metaevent. + */ +int +smf_event_is_tempo_change_or_time_signature(const smf_event_t *event) +{ + if (!smf_event_is_metadata(event)) + return 0; + + assert(event->midi_buffer_length >= 2); + + if (event->midi_buffer[1] == 0x51 || event->midi_buffer[1] == 0x58) + return 1; + + return 0; +} + +/** + * Sets "Format" field of MThd header to the specified value. Note that you + * don't really need to use this, as libsmf will automatically change format + * from 0 to 1 when you add the second track. + * \param smf SMF. + * \param format 0 for one track per file, 1 for several tracks per file. + */ +int +smf_set_format(smf_t *smf, int format) +{ + assert(format == 0 || format == 1); + + if (smf->number_of_tracks > 1 && format == 0) { + g_critical("There is more than one track, cannot set format to 0."); + return -1; + } + + smf->format = format; + + return 0; +} + +/** + * Sets the PPQN ("Division") field of MThd header. This is mandatory, you + * should call it right after smf_new. Note that changing PPQN will change time_seconds + * of all the events. + * \param smf SMF. + * \param ppqn New PPQN. + */ +int +smf_set_ppqn(smf_t *smf, int ppqn) +{ + assert(ppqn > 0); + + smf->ppqn = ppqn; + + return 0; +} + +/** + * Returns next event from the track given and advances next event counter. + * Do not depend on End Of Track event being the last event on the track - it + * is possible that the track will not end with EOT if you haven't added it + * yet. EOTs are added automatically during smf_save(). + * + * \return Event or NULL, if there are no more events left in this track. + */ +smf_event_t * +smf_track_get_next_event(smf_track_t *track) +{ + smf_event_t *event, *next_event; + + /* End of track? */ + if (track->next_event_number == -1) + return NULL; + + assert(track->next_event_number >= 1); + assert(track->number_of_events > 0); + + event = smf_track_get_event_by_number(track, track->next_event_number); + + assert(event != NULL); + + /* Is this the last event in the track? */ + if (track->next_event_number < track->number_of_events) { + next_event = smf_track_get_event_by_number(track, track->next_event_number + 1); + assert(next_event); + + track->time_of_next_event = next_event->time_pulses; + track->next_event_number++; + } else { + track->next_event_number = -1; + } + + return event; +} + +/** + * Returns next event from the track given. Does not change next event counter, + * so repeatedly calling this routine will return the same event. + * \return Event or NULL, if there are no more events left in this track. + */ +static smf_event_t * +smf_peek_next_event_from_track(smf_track_t *track) +{ + smf_event_t *event; + + /* End of track? */ + if (track->next_event_number == -1) + return NULL; + + assert(track->next_event_number >= 1); + assert(track->events_array->len != 0); + + event = smf_track_get_event_by_number(track, track->next_event_number); + + return event; +} + +/** + * \return Track with a given number or NULL, if there is no such track. + * Tracks are numbered consecutively starting from one. + */ +smf_track_t * +smf_get_track_by_number(const smf_t *smf, int track_number) +{ + smf_track_t *track; + + assert(track_number >= 1); + + if (track_number > smf->number_of_tracks) + return NULL; + + track = (smf_track_t *)g_ptr_array_index(smf->tracks_array, track_number - 1); + + assert(track); + + return track; +} + +/** + * \return Event with a given number or NULL, if there is no such event. + * Events are numbered consecutively starting from one. + */ +smf_event_t * +smf_track_get_event_by_number(const smf_track_t *track, int event_number) +{ + smf_event_t *event; + + assert(event_number >= 1); + + if (event_number > track->number_of_events) + return NULL; + + event = g_ptr_array_index(track->events_array, event_number - 1); + + assert(event); + + return event; +} + +/** + * \return Last event on the track or NULL, if track is empty. + */ +smf_event_t * +smf_track_get_last_event(const smf_track_t *track) +{ + smf_event_t *event; + + if (track->number_of_events == 0) + return NULL; + + event = smf_track_get_event_by_number(track, track->number_of_events); + + return event; +} + +/** + * Searches for track that contains next event, in time order. In other words, + * returns the track that contains event that should be played next. + * \return Track with next event or NULL, if there are no events left. + */ +smf_track_t * +smf_find_track_with_next_event(smf_t *smf) +{ + int i, min_time = 0; + smf_track_t *track = NULL, *min_time_track = NULL; + + /* Find track with event that should be played next. */ + for (i = 1; i <= smf->number_of_tracks; i++) { + track = smf_get_track_by_number(smf, i); + + assert(track); + + /* No more events in this track? */ + if (track->next_event_number == -1) + continue; + + if (track->time_of_next_event < min_time || min_time_track == NULL) { + min_time = track->time_of_next_event; + min_time_track = track; + } + } + + return min_time_track; +} + +/** + * \return Next event, in time order, or NULL, if there are none left. + */ +smf_event_t * +smf_get_next_event(smf_t *smf) +{ + smf_event_t *event; + smf_track_t *track = smf_find_track_with_next_event(smf); + + if (track == NULL) { +#if 0 + g_debug("End of the song."); +#endif + + return NULL; + } + + event = smf_track_get_next_event(track); + + assert(event != NULL); + + event->track->smf->last_seek_position = -1.0; + + return event; +} + +/** + * \return Next event, in time order, or NULL, if there are none left. Does + * not advance position in song. + */ +smf_event_t * +smf_peek_next_event(smf_t *smf) +{ + smf_event_t *event; + smf_track_t *track = smf_find_track_with_next_event(smf); + + if (track == NULL) { +#if 0 + g_debug("End of the song."); +#endif + + return NULL; + } + + event = smf_peek_next_event_from_track(track); + + assert(event != NULL); + + return event; +} + +/** + * Rewinds the SMF. What that means is, after calling this routine, smf_get_next_event + * will return first event in the song. + */ +void +smf_rewind(smf_t *smf) +{ + int i; + smf_track_t *track = NULL; + smf_event_t *event; + + assert(smf); + + smf->last_seek_position = 0.0; + + for (i = 1; i <= smf->number_of_tracks; i++) { + track = smf_get_track_by_number(smf, i); + + assert(track != NULL); + + if (track->number_of_events > 0) { + track->next_event_number = 1; + event = smf_peek_next_event_from_track(track); + assert(event); + track->time_of_next_event = event->time_pulses; + } else { + track->next_event_number = -1; + track->time_of_next_event = 0; +#if 0 + g_warning("Warning: empty track."); +#endif + } + } +} + +/** + * Seeks the SMF to the given event. After calling this routine, smf_get_next_event + * will return the event that was the second argument of this call. + */ +int +smf_seek_to_event(smf_t *smf, const smf_event_t *target) +{ + smf_event_t *event; + + smf_rewind(smf); + +#if 0 + g_debug("Seeking to event %d, track %d.", target->event_number, target->track->track_number); +#endif + + for (;;) { + event = smf_peek_next_event(smf); + + /* There can't be NULL here, unless "target" is not in this smf. */ + assert(event); + + if (event != target) + smf_get_next_event(smf); + else + break; + } + + smf->last_seek_position = event->time_seconds; + + return 0; +} + +/** + * Seeks the SMF to the given position. For example, after seeking to 1.0 seconds, + * smf_get_next_event will return first event that happens after the first second of song. + */ +int +smf_seek_to_seconds(smf_t *smf, double seconds) +{ + smf_event_t *event; + + assert(seconds >= 0.0); + + if (seconds == smf->last_seek_position) { +#if 0 + g_debug("Avoiding seek to %f seconds.", seconds); +#endif + return 0; + } + + smf_rewind(smf); + +#if 0 + g_debug("Seeking to %f seconds.", seconds); +#endif + + for (;;) { + event = smf_peek_next_event(smf); + + if (event == NULL) { + g_critical("Trying to seek past the end of song."); + return -1; + } + + if (event->time_seconds < seconds) + smf_get_next_event(smf); + else + break; + } + + smf->last_seek_position = seconds; + + return 0; +} + +/** + * Seeks the SMF to the given position. For example, after seeking to 10 pulses, + * smf_get_next_event will return first event that happens after the first ten pulses. + */ +int +smf_seek_to_pulses(smf_t *smf, int pulses) +{ + smf_event_t *event; + + assert(pulses >= 0); + + smf_rewind(smf); + +#if 0 + g_debug("Seeking to %d pulses.", pulses); +#endif + + for (;;) { + event = smf_peek_next_event(smf); + + if (event == NULL) { + g_critical("Trying to seek past the end of song."); + return -1; + } + + if (event->time_pulses < pulses) + smf_get_next_event(smf); + else + break; + } + + smf->last_seek_position = event->time_seconds; + + return 0; +} + +/** + * \return Length of SMF, in pulses. + */ +int +smf_get_length_pulses(const smf_t *smf) +{ + int pulses = 0, i; + + for (i = 1; i <= smf->number_of_tracks; i++) { + smf_track_t *track; + smf_event_t *event; + + track = smf_get_track_by_number(smf, i); + assert(track); + + event = smf_track_get_last_event(track); + /* Empty track? */ + if (event == NULL) + continue; + + if (event->time_pulses > pulses) + pulses = event->time_pulses; + } + + return pulses; +} + +/** + * \return Length of SMF, in seconds. + */ +double +smf_get_length_seconds(const smf_t *smf) +{ + int i; + double seconds = 0.0; + + for (i = 1; i <= smf->number_of_tracks; i++) { + smf_track_t *track; + smf_event_t *event; + + track = smf_get_track_by_number(smf, i); + assert(track); + + event = smf_track_get_last_event(track); + /* Empty track? */ + if (event == NULL) + continue; + + if (event->time_seconds > seconds) + seconds = event->time_seconds; + } + + return seconds; +} + +/** + * \return Nonzero, if there are no events in the SMF after this one. + * Note that may be more than one "last event", if they occur at the same time. + */ +int +smf_event_is_last(const smf_event_t *event) +{ + if (smf_get_length_pulses(event->track->smf) <= event->time_pulses) + return 1; + + return 0; +} + +/** + * \return Version of libsmf. + */ +const char * +smf_get_version(void) +{ + return SMF_VERSION; +} + diff --git a/libsmf/smf.h b/libsmf/smf.h new file mode 100644 index 0000000..90a5e59 --- /dev/null +++ b/libsmf/smf.h @@ -0,0 +1,343 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * ALTHOUGH THIS SOFTWARE IS MADE OF WIN AND SCIENCE, IT IS PROVIDED BY THE + * AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +/* + * This is the include file for libsmf, Standard Midi File format library. + * For questions and comments, contact Edward Tomasz Napierala . + */ + +/** + * + * \mainpage General usage instructions + * + * An smf_t structure represents a "song". Every valid smf contains one or more tracks. + * Tracks contain zero or more events. Libsmf doesn't care about actual MIDI data, as long + * as it is valid from the MIDI specification point of view - it may be realtime message, + * SysEx, whatever. + * + * The only field in smf_t, smf_track_t, smf_event_t and smf_tempo_t structures your + * code may modify is event->midi_buffer and event->midi_buffer_length. Do not modify + * other fields, _ever_. You may read them, though. + * + * Say you want to load a Standard MIDI File (.mid) file and play it back somehow. This is (roughly) + * how you do this: + * + * \code + * smf_t *smf = smf_load(file_name); + * if (smf == NULL) { + * Whoops, something went wrong. + * return; + * } + * + * for (;;) { + * smf_event_t *event = smf_get_next_event(smf); + * if (event == NULL) { + * No more events, end of the song. + * return; + * } + * + * if (smf_event_is_metadata(event)) + * continue; + * + * wait until event->time_seconds. + * feed_to_midi_output(event->midi_buffer, event->midi_buffer_length); + * } + * + * \endcode + * + * Saving works like this: + * + * \code + * + * smf_t *smf = smf_new(); + * if (smf == NULL) { + * Whoops. + * return; + * } + * + * for (int i = 1; i <= number of tracks; i++) { + * smf_track_t *track = smf_track_new(); + * if (track == NULL) { + * Whoops. + * return; + * } + * + * smf_add_track(smf, track); + * + * for (int j = 1; j <= number of events in this track; j++) { + * smf_event_t *event = smf_event_new_from_pointer(your MIDI message, message length); + * if (event == NULL) { + * Whoops. + * return; + * } + * + * smf_track_add_event_seconds(track, event, seconds since start of the song); + * } + * } + * + * ret = smf_save(smf, file_name); + * if (ret) { + * Whoops, saving failed for some reason. + * return; + * } + * + * \endcode + * + * There are two basic ways of getting MIDI data out of smf - sequential or by track/event number. You may + * mix them if you need to. First one is used in the example above - seek to the point from which you want + * the playback to start (using smf_seek_to_seconds, for example) and then do smf_get_next_event in loop, + * until it returns NULL. After smf_load smf is rewound to the start of the song. + * + * Getting events by number works like this: + * + * \code + * + * smf_track_t *track = smf_get_track_by_number(smf, track_number); + * smf_event_t *event = smf_track_get_event_by_number(track, event_number); + * + * \endcode + * + * Tracks and events are numbered consecutively, starting from one. If you remove a track or event, + * the rest of tracks/events will be renumbered. To get number of event in its track, use event->event_number. + * To get the number of track in its smf, use track->track_number. To get the number of events in the track, + * use track->number_of_events. To get the number of tracks in the smf, use smf->number_of_tracks. + * + * In SMF File Format, each track has to end with End Of Track metaevent. If you load SMF file using smf_load, + * that will be the case. If you want to create or edit an SMF, you don't need to worry about EOT events, + * libsmf automatically takes care of them for you. If you try to save an SMF with tracks that do not end + * with EOTs, smf_save will append them. If you try to add event that happens after EOT metaevent, libsmf + * will remove the EOT. If you want to add EOT automatically, you can, of course, using smf_track_add_eot_seconds + * or smf_track_add_eot_pulses. + * + * Each event carries three time values - event->time_seconds, which is seconds since the start of the song, + * event->time_pulses, which is PPQN clocks since the start of the song, and event->delta_pulses, which is PPQN clocks + * since the previous event in that track. These values are invalid if the event is not attached to the track. + * If event is attached, all three values are valid. Time of the event is specified when adding the event + * (smf_track_add_event_seconds/smf_track_add_event_pulses/smf_track_add_event_delta_pulses); the remaining + * two values are computed from that. + * + * Tempo related stuff happens automatically - when you add a metaevent that + * is Tempo Change or Time Signature, libsmf adds that event to the tempo map. If you remove + * Tempo Change event that is in the middle of the song, the rest of the events will have their + * event->time_seconds recomputed from event->time_pulses before smf_track_remove_event function returns. + * Adding Tempo Change in the middle of the song works in a similar way. + * + * MIDI data (event->midi_buffer) are always in normalized form - they always begin with status byte + * (no running status), there are no system realtime events embedded in them etc. Events like SysExes + * are in "on the wire" form, without embedded length that is used in SMF file format. Obviously + * libsmf "normalizes" MIDI data during loading and "denormalizes" (adding length to SysExes, escaping + * System Common and System Realtime messages etc) during writing. + * + * Note that you always have to first add the track to smf, and then add events to the track. + * Doing it the other way around will trip asserts. Also, try to add events at the end of the track and remove + * them from the end of the track, that's much more efficient. + * + * All the libsmf functions have prefix "smf_". Library does not use any global variables and is thread-safe, + * as long as you don't try to work on the same SMF (smf_t and it's descendant tracks and events) from several + * threads at once without protecting it with mutex. Library depends on glib and nothing else. License is + * BSD, two clause. + * + */ + +#ifndef SMF_H +#define SMF_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +struct smf_struct { + int format; + int expected_number_of_tracks; + + /** These fields are extracted from "division" field of MThd header. Valid is _either_ ppqn or frames_per_second/resolution. */ + int ppqn; + int frames_per_second; + int resolution; + int number_of_tracks; + + /** These are private fields using only by loading and saving routines. */ + FILE *stream; + void *file_buffer; + int file_buffer_length; + int next_chunk_offset; + + /** Private, used by smf.c. */ + GPtrArray *tracks_array; + double last_seek_position; + + /** Private, used by smf_tempo.c. */ + /** Array of pointers to smf_tempo_struct. */ + GPtrArray *tempo_array; +}; + +typedef struct smf_struct smf_t; + +/** This structure describes a single tempo change. */ +struct smf_tempo_struct { + int time_pulses; + double time_seconds; + int microseconds_per_quarter_note; + int numerator; + int denominator; + int clocks_per_click; + int notes_per_note; +}; + +typedef struct smf_tempo_struct smf_tempo_t; + +struct smf_track_struct { + smf_t *smf; + + int track_number; + int number_of_events; + + /** These are private fields using only by loading and saving routines. */ + void *file_buffer; + int file_buffer_length; + int last_status; /* Used for "running status". */ + + /** Private, used by smf.c. */ + /** Offset into buffer, used in parse_next_event(). */ + int next_event_offset; + int next_event_number; + + /** Absolute time of next event on events_queue. */ + int time_of_next_event; + GPtrArray *events_array; +}; + +typedef struct smf_track_struct smf_track_t; + +struct smf_event_struct { + /** Pointer to the track, or NULL if event is not attached. */ + smf_track_t *track; + + /** Number of this event in the track. Events are numbered consecutively, starting from one. */ + int event_number; + + /** Note that the time fields are invalid, if event is not attached to a track. */ + /** Time, in pulses, since the previous event on this track. */ + int delta_time_pulses; + + /** Time, in pulses, since the start of the song. */ + int time_pulses; + + /** Time, in seconds, since the start of the song. */ + double time_seconds; + + /** Tracks are numbered consecutively, starting from 1. */ + int track_number; + + /** Pointer to the buffer containing MIDI message. This is freed by smf_event_delete. */ + unsigned char *midi_buffer; + + /** Length of the MIDI message in the buffer, in bytes. */ + int midi_buffer_length; +}; + +typedef struct smf_event_struct smf_event_t; + +/* Routines for manipulating smf_t. */ +smf_t *smf_new(void); +void smf_delete(smf_t *smf); + +int smf_set_format(smf_t *smf, int format); +int smf_set_ppqn(smf_t *smf, int format); + +char *smf_decode(const smf_t *smf); + +smf_track_t *smf_get_track_by_number(const smf_t *smf, int track_number); +smf_event_t *smf_get_next_event(smf_t *smf); +smf_event_t *smf_peek_next_event(smf_t *smf); +void smf_rewind(smf_t *smf); +int smf_seek_to_seconds(smf_t *smf, double seconds); +int smf_seek_to_pulses(smf_t *smf, int pulses); +int smf_seek_to_event(smf_t *smf, const smf_event_t *event); + +int smf_get_length_pulses(const smf_t *smf); +double smf_get_length_seconds(const smf_t *smf); +int smf_event_is_last(const smf_event_t *event); + +void smf_add_track(smf_t *smf, smf_track_t *track); +void smf_remove_track(smf_track_t *track); + +/* Routines for manipulating smf_track_t. */ +smf_track_t *smf_track_new(void); +void smf_track_delete(smf_track_t *track); + +smf_event_t *smf_track_get_next_event(smf_track_t *track); +smf_event_t *smf_track_get_event_by_number(const smf_track_t *track, int event_number); +smf_event_t *smf_track_get_last_event(const smf_track_t *track); + +void smf_track_add_event_delta_pulses(smf_track_t *track, smf_event_t *event, int pulses); +void smf_track_add_event_pulses(smf_track_t *track, smf_event_t *event, int pulses); +void smf_track_add_event_seconds(smf_track_t *track, smf_event_t *event, double seconds); +int smf_track_add_eot_delta_pulses(smf_track_t *track, int delta); +int smf_track_add_eot_pulses(smf_track_t *track, int pulses); +int smf_track_add_eot_seconds(smf_track_t *track, double seconds); +void smf_track_remove_event(smf_event_t *event); + +/* Routines for manipulating smf_event_t. */ +smf_event_t *smf_event_new(void); +smf_event_t *smf_event_new_from_pointer(void *midi_data, int len); +smf_event_t *smf_event_new_from_bytes(int first_byte, int second_byte, int third_byte); +void smf_event_delete(smf_event_t *event); + +int smf_event_is_valid(const smf_event_t *event); +int smf_event_is_metadata(const smf_event_t *event); +int smf_event_is_system_realtime(const smf_event_t *event); +int smf_event_is_system_common(const smf_event_t *event); +int smf_event_is_eot(const smf_event_t *event); +char *smf_event_decode(const smf_event_t *event); +char *smf_string_from_event(const smf_event_t *event); + +/* Routines for loading SMF files. */ +smf_t *smf_load(const char *file_name); +smf_t *smf_load_from_memory(const void *buffer, const int buffer_length); + +/* Routine for writing SMF files. */ +int smf_save(smf_t *smf, const char *file_name); + +/* Routines for manipulating smf_tempo_t. */ +smf_tempo_t *smf_get_tempo_by_pulses(const smf_t *smf, int pulses); +smf_tempo_t *smf_get_tempo_by_seconds(const smf_t *smf, double seconds); +smf_tempo_t *smf_get_tempo_by_number(const smf_t *smf, int number); +smf_tempo_t *smf_get_last_tempo(const smf_t *smf); + +const char *smf_get_version(void); + +#ifdef __cplusplus +} +#endif + +#endif /* SMF_H */ + diff --git a/libsmf/smf_decode.c b/libsmf/smf_decode.c new file mode 100644 index 0000000..3cd762c --- /dev/null +++ b/libsmf/smf_decode.c @@ -0,0 +1,624 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * ALTHOUGH THIS SOFTWARE IS MADE OF WIN AND SCIENCE, IT IS PROVIDED BY THE + * AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +/* + * This is Standard MIDI File format implementation, event decoding routines. + * + * For questions and comments, contact Edward Tomasz Napierala . + */ + +#include +#include +#include +#include +#include +#include +#include +#include "smf.h" +#include "smf_private.h" + +#define BUFFER_SIZE 1024 + +/** + * \return Nonzero if event is metaevent. You should never send metaevents; + * they are not really MIDI messages. They carry information like track title, + * time signature etc. + */ +int +smf_event_is_metadata(const smf_event_t *event) +{ + assert(event->midi_buffer); + assert(event->midi_buffer_length > 0); + + if (event->midi_buffer[0] == 0xFF) + return 1; + + return 0; +} + +/** + * \return Nonzero if event is system realtime. + */ +int +smf_event_is_system_realtime(const smf_event_t *event) +{ + assert(event->midi_buffer); + assert(event->midi_buffer_length > 0); + + if (smf_event_is_metadata(event)) + return 0; + + if (event->midi_buffer[0] >= 0xF8) + return 1; + + return 0; +} + +/** + * \return Nonzero if event is system common. + */ +int +smf_event_is_system_common(const smf_event_t *event) +{ + assert(event->midi_buffer); + assert(event->midi_buffer_length > 0); + + if (event->midi_buffer[0] >= 0xF0 && event->midi_buffer[0] <= 0xF7) + return 1; + + return 0; +} +/** + * \return Nonzero if event is SysEx message. + */ +int +smf_event_is_sysex(const smf_event_t *event) +{ + assert(event->midi_buffer); + assert(event->midi_buffer_length > 0); + + if (event->midi_buffer[0] == 0xF0) + return 1; + + return 0; +} + +static char * +smf_event_decode_textual(const smf_event_t *event, const char *name) +{ + int off = 0; + char *buf, *extracted; + + buf = malloc(BUFFER_SIZE); + if (buf == NULL) { + g_critical("smf_event_decode_textual: malloc failed."); + return NULL; + } + + extracted = smf_string_from_event(event); + if (extracted == NULL) { + free(buf); + return NULL; + } + + snprintf(buf + off, BUFFER_SIZE - off, "%s: %s", name, extracted); + + return buf; +} + +static char * +smf_event_decode_metadata(const smf_event_t *event) +{ + int off = 0, mspqn, flats, isminor; + char *buf; + + static const char *const major_keys[] = {"Fb", "Cb", "Gb", "Db", "Ab", + "Eb", "Bb", "F", "C", "G", "D", "A", "E", "B", "F#", "C#", "G#"}; + + static const char *const minor_keys[] = {"Dbm", "Abm", "Ebm", "Bbm", "Fm", + "Cm", "Gm", "Dm", "Am", "Em", "Bm", "F#m", "C#m", "G#m", "D#m", "A#m", "E#m"}; + + assert(smf_event_is_metadata(event)); + + switch (event->midi_buffer[1]) { + case 0x01: + return smf_event_decode_textual(event, "Text"); + + case 0x02: + return smf_event_decode_textual(event, "Copyright"); + + case 0x03: + return smf_event_decode_textual(event, "Sequence/Track Name"); + + case 0x04: + return smf_event_decode_textual(event, "Instrument"); + + case 0x05: + return smf_event_decode_textual(event, "Lyric"); + + case 0x06: + return smf_event_decode_textual(event, "Marker"); + + case 0x07: + return smf_event_decode_textual(event, "Cue Point"); + + case 0x08: + return smf_event_decode_textual(event, "Program Name"); + + case 0x09: + return smf_event_decode_textual(event, "Device (Port) Name"); + + default: + break; + } + + buf = malloc(BUFFER_SIZE); + if (buf == NULL) { + g_critical("smf_event_decode_metadata: malloc failed."); + return NULL; + } + + switch (event->midi_buffer[1]) { + case 0x00: + off += snprintf(buf + off, BUFFER_SIZE - off, "Sequence number"); + break; + + /* http://music.columbia.edu/pipermail/music-dsp/2004-August/061196.html */ + case 0x20: + if (event->midi_buffer_length < 4) { + g_critical("smf_event_decode_metadata: truncated MIDI message."); + goto error; + } + + off += snprintf(buf + off, BUFFER_SIZE - off, "Channel Prefix: %d.", event->midi_buffer[3]); + break; + + case 0x21: + if (event->midi_buffer_length < 4) { + g_critical("smf_event_decode_metadata: truncated MIDI message."); + goto error; + } + + off += snprintf(buf + off, BUFFER_SIZE - off, "Midi Port: %d.", event->midi_buffer[3]); + break; + + case 0x2F: + off += snprintf(buf + off, BUFFER_SIZE - off, "End Of Track"); + break; + + case 0x51: + if (event->midi_buffer_length < 6) { + g_critical("smf_event_decode_metadata: truncated MIDI message."); + goto error; + } + + mspqn = (event->midi_buffer[3] << 16) + (event->midi_buffer[4] << 8) + event->midi_buffer[5]; + + off += snprintf(buf + off, BUFFER_SIZE - off, "Tempo: %d microseconds per quarter note, %.2f BPM", + mspqn, 60000000.0 / (double)mspqn); + break; + + case 0x54: + off += snprintf(buf + off, BUFFER_SIZE - off, "SMPTE Offset"); + break; + + case 0x58: + if (event->midi_buffer_length < 7) { + g_critical("smf_event_decode_metadata: truncated MIDI message."); + goto error; + } + + off += snprintf(buf + off, BUFFER_SIZE - off, + "Time Signature: %d/%d, %d clocks per click, %d notated 32nd notes per quarter note", + event->midi_buffer[3], (int)pow(2, event->midi_buffer[4]), event->midi_buffer[5], + event->midi_buffer[6]); + break; + + case 0x59: + if (event->midi_buffer_length < 5) { + g_critical("smf_event_decode_metadata: truncated MIDI message."); + goto error; + } + + flats = event->midi_buffer[3]; + isminor = event->midi_buffer[4]; + + if (isminor != 0 && isminor != 1) { + g_critical("smf_event_decode_metadata: last byte of the Key Signature event has invalid value %d.", isminor); + goto error; + } + + off += snprintf(buf + off, BUFFER_SIZE - off, "Key Signature: "); + + if (flats > 8 && flats < 248) { + off += snprintf(buf + off, BUFFER_SIZE - off, "%d %s, %s key", abs((int8_t)flats), + flats > 127 ? "flats" : "sharps", isminor ? "minor" : "major"); + } else { + int i = (flats - 248) & 255; + + assert(i >= 0 && i < sizeof(minor_keys) / sizeof(*minor_keys)); + assert(i >= 0 && i < sizeof(major_keys) / sizeof(*major_keys)); + + if (isminor) + off += snprintf(buf + off, BUFFER_SIZE - off, "%s", minor_keys[i]); + else + off += snprintf(buf + off, BUFFER_SIZE - off, "%s", major_keys[i]); + } + + break; + + case 0x7F: + off += snprintf(buf + off, BUFFER_SIZE - off, "Proprietary (aka Sequencer) Event, length %d", + event->midi_buffer_length); + break; + + default: + goto error; + } + + return buf; + +error: + free(buf); + + return NULL; +} + +static char * +smf_event_decode_system_realtime(const smf_event_t *event) +{ + int off = 0; + char *buf; + + assert(smf_event_is_system_realtime(event)); + + if (event->midi_buffer_length != 1) { + g_critical("smf_event_decode_system_realtime: event length is not 1."); + return NULL; + } + + buf = malloc(BUFFER_SIZE); + if (buf == NULL) { + g_critical("smf_event_decode_system_realtime: malloc failed."); + return NULL; + } + + switch (event->midi_buffer[0]) { + case 0xF8: + off += snprintf(buf + off, BUFFER_SIZE - off, "MIDI Clock (realtime)"); + break; + + case 0xF9: + off += snprintf(buf + off, BUFFER_SIZE - off, "Tick (realtime)"); + break; + + case 0xFA: + off += snprintf(buf + off, BUFFER_SIZE - off, "MIDI Start (realtime)"); + break; + + case 0xFB: + off += snprintf(buf + off, BUFFER_SIZE - off, "MIDI Continue (realtime)"); + break; + + case 0xFC: + off += snprintf(buf + off, BUFFER_SIZE - off, "MIDI Stop (realtime)"); + break; + + case 0xFE: + off += snprintf(buf + off, BUFFER_SIZE - off, "Active Sense (realtime)"); + break; + + default: + free(buf); + return NULL; + } + + return buf; +} + +static char * +smf_event_decode_sysex(const smf_event_t *event) +{ + int off = 0; + char *buf, manufacturer, subid, subid2; + + assert(smf_event_is_sysex(event)); + + if (event->midi_buffer_length < 5) { + g_critical("smf_event_decode_sysex: truncated MIDI message."); + return NULL; + } + + buf = malloc(BUFFER_SIZE); + if (buf == NULL) { + g_critical("smf_event_decode_sysex: malloc failed."); + return NULL; + } + + manufacturer = event->midi_buffer[1]; + + if (manufacturer == 0x7F) { + off += snprintf(buf + off, BUFFER_SIZE - off, "SysEx, realtime, channel %d", event->midi_buffer[2]); + } else if (manufacturer == 0x7E) { + off += snprintf(buf + off, BUFFER_SIZE - off, "SysEx, non-realtime, channel %d", event->midi_buffer[2]); + } else { + off += snprintf(buf + off, BUFFER_SIZE - off, "SysEx, manufacturer 0x%x", manufacturer); + + return buf; + } + + subid = event->midi_buffer[3]; + subid2 = event->midi_buffer[4]; + + if (subid == 0x01) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Sample Dump Header"); + + else if (subid == 0x02) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Sample Dump Data Packet"); + + else if (subid == 0x03) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Sample Dump Request"); + + else if (subid == 0x04 && subid2 == 0x01) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Master Volume"); + + else if (subid == 0x05 && subid2 == 0x01) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Sample Dump Loop Point Retransmit"); + + else if (subid == 0x05 && subid2 == 0x02) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Sample Dump Loop Point Request"); + + else if (subid == 0x06 && subid2 == 0x01) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Identity Request"); + + else if (subid == 0x06 && subid2 == 0x02) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Identity Reply"); + + else if (subid == 0x08 && subid2 == 0x00) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Bulk Tuning Dump Request"); + + else if (subid == 0x08 && subid2 == 0x01) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Bulk Tuning Dump"); + + else if (subid == 0x08 && subid2 == 0x02) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Single Note Tuning Change"); + + else if (subid == 0x08 && subid2 == 0x03) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Bulk Tuning Dump Request (Bank)"); + + else if (subid == 0x08 && subid2 == 0x04) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Key Based Tuning Dump"); + + else if (subid == 0x08 && subid2 == 0x05) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Scale/Octave Tuning Dump, 1 byte format"); + + else if (subid == 0x08 && subid2 == 0x06) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Scale/Octave Tuning Dump, 2 byte format"); + + else if (subid == 0x08 && subid2 == 0x07) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Single Note Tuning Change (Bank)"); + + else if (subid == 0x09) + off += snprintf(buf + off, BUFFER_SIZE - off, ", General Midi %s", subid2 == 0 ? "disable" : "enable"); + + else if (subid == 0x7C) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Sample Dump Wait"); + + else if (subid == 0x7D) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Sample Dump Cancel"); + + else if (subid == 0x7E) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Sample Dump NAK"); + + else if (subid == 0x7F) + off += snprintf(buf + off, BUFFER_SIZE - off, ", Sample Dump ACK"); + + else + off += snprintf(buf + off, BUFFER_SIZE - off, ", Unknown"); + + return buf; +} + +static char * +smf_event_decode_system_common(const smf_event_t *event) +{ + int off = 0; + char *buf; + + assert(smf_event_is_system_common(event)); + + if (smf_event_is_sysex(event)) + return smf_event_decode_sysex(event); + + buf = malloc(BUFFER_SIZE); + if (buf == NULL) { + g_critical("smf_event_decode_system_realtime: malloc failed."); + return NULL; + } + + switch (event->midi_buffer[0]) { + case 0xF1: + off += snprintf(buf + off, BUFFER_SIZE - off, "MTC Quarter Frame"); + break; + + case 0xF2: + off += snprintf(buf + off, BUFFER_SIZE - off, "Song Position Pointer"); + break; + + case 0xF3: + off += snprintf(buf + off, BUFFER_SIZE - off, "Song Select"); + break; + + case 0xF6: + off += snprintf(buf + off, BUFFER_SIZE - off, "Tune Request"); + break; + + default: + free(buf); + return NULL; + } + + return buf; +} + +static void +note_from_int(char *buf, int note_number) +{ + int note, octave; + char *names[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}; + + octave = note_number / 12 - 1; + note = note_number % 12; + + sprintf(buf, "%s%d", names[note], octave); +} + +/** + * \return Textual representation of the event given, or NULL, if event is unknown. + * Returned string looks like this: + * Note On, channel 0, note F#3, velocity 0 + */ +char * +smf_event_decode(const smf_event_t *event) +{ + int off = 0; + char *buf, note[5]; + + if (smf_event_is_metadata(event)) + return smf_event_decode_metadata(event); + + if (smf_event_is_system_realtime(event)) + return smf_event_decode_system_realtime(event); + + if (smf_event_is_system_common(event)) + return smf_event_decode_system_common(event); + + if (!smf_event_length_is_valid(event)) { + g_critical("smf_event_decode: incorrect MIDI message length."); + return NULL; + } + + buf = malloc(BUFFER_SIZE); + if (buf == NULL) { + g_critical("smf_event_decode: malloc failed."); + return NULL; + } + + switch (event->midi_buffer[0] & 0xF0) { + case 0x80: + note_from_int(note, event->midi_buffer[1]); + off += snprintf(buf + off, BUFFER_SIZE - off, "Note Off, channel %d, note %s, velocity %d", + event->midi_buffer[0] & 0x0F, note, event->midi_buffer[2]); + break; + + case 0x90: + note_from_int(note, event->midi_buffer[1]); + off += snprintf(buf + off, BUFFER_SIZE - off, "Note On, channel %d, note %s, velocity %d", + event->midi_buffer[0] & 0x0F, note, event->midi_buffer[2]); + break; + + case 0xA0: + note_from_int(note, event->midi_buffer[1]); + off += snprintf(buf + off, BUFFER_SIZE - off, "Aftertouch, channel %d, note %s, pressure %d", + event->midi_buffer[0] & 0x0F, note, event->midi_buffer[2]); + break; + + case 0xB0: + off += snprintf(buf + off, BUFFER_SIZE - off, "Controller, channel %d, controller %d, value %d", + event->midi_buffer[0] & 0x0F, event->midi_buffer[1], event->midi_buffer[2]); + break; + + case 0xC0: + off += snprintf(buf + off, BUFFER_SIZE - off, "Program Change, channel %d, controller %d", + event->midi_buffer[0] & 0x0F, event->midi_buffer[1]); + break; + + case 0xD0: + off += snprintf(buf + off, BUFFER_SIZE - off, "Channel Pressure, channel %d, pressure %d", + event->midi_buffer[0] & 0x0F, event->midi_buffer[1]); + break; + + case 0xE0: + off += snprintf(buf + off, BUFFER_SIZE - off, "Pitch Wheel, channel %d, value %d", + event->midi_buffer[0] & 0x0F, ((int)event->midi_buffer[2] << 7) | (int)event->midi_buffer[2]); + break; + + default: + free(buf); + return NULL; + } + + return buf; +} + +/** + * \return Textual representation of the data extracted from MThd header, or NULL, if something goes wrong. + * Returned string looks like this: + * format: 1 (several simultaneous tracks); number of tracks: 4; division: 192 PPQN. + */ +char * +smf_decode(const smf_t *smf) +{ + int off = 0; + char *buf; + + buf = malloc(BUFFER_SIZE); + if (buf == NULL) { + g_critical("smf_event_decode: malloc failed."); + return NULL; + } + + off += snprintf(buf + off, BUFFER_SIZE - off, "format: %d ", smf->format); + + switch (smf->format) { + case 0: + off += snprintf(buf + off, BUFFER_SIZE - off, "(single track)"); + break; + + case 1: + off += snprintf(buf + off, BUFFER_SIZE - off, "(several simultaneous tracks)"); + break; + + case 2: + off += snprintf(buf + off, BUFFER_SIZE - off, "(several independent tracks)"); + break; + + default: + off += snprintf(buf + off, BUFFER_SIZE - off, "(INVALID FORMAT)"); + break; + } + + off += snprintf(buf + off, BUFFER_SIZE - off, "; number of tracks: %d", smf->number_of_tracks); + + if (smf->ppqn != 0) + off += snprintf(buf + off, BUFFER_SIZE - off, "; division: %d PPQN", smf->ppqn); + else + off += snprintf(buf + off, BUFFER_SIZE - off, "; division: %d FPS, %d resolution", smf->frames_per_second, smf->resolution); + + return buf; +} + diff --git a/libsmf/smf_load.c b/libsmf/smf_load.c new file mode 100644 index 0000000..e918066 --- /dev/null +++ b/libsmf/smf_load.c @@ -0,0 +1,890 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * ALTHOUGH THIS SOFTWARE IS MADE OF WIN AND SCIENCE, IT IS PROVIDED BY THE + * AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +/* + * This is Standard MIDI File loader. + * + * For questions and comments, contact Edward Tomasz Napierala . + */ + +/* Reference: http://www.borg.com/~jglatt/tech/midifile.htm */ + +#include +#include +#include +#include +#include +#include +#include +#include "smf.h" +#include "smf_private.h" + +/** + * Returns pointer to the next SMF chunk in smf->buffer, based on length of the previous one. + * Returns NULL in case of error. + */ +static struct chunk_header_struct * +next_chunk(smf_t *smf) +{ + struct chunk_header_struct *chunk; + void *next_chunk_ptr; + + assert(smf->file_buffer != NULL); + assert(smf->file_buffer_length > 0); + assert(smf->next_chunk_offset >= 0); + + if (smf->next_chunk_offset + sizeof(struct chunk_header_struct) >= smf->file_buffer_length) { + g_critical("SMF warning: no more chunks left."); + return NULL; + } + + next_chunk_ptr = (unsigned char *)smf->file_buffer + smf->next_chunk_offset; + + chunk = (struct chunk_header_struct *)next_chunk_ptr; + + if (!isalpha(chunk->id[0]) || !isalpha(chunk->id[1]) || !isalpha(chunk->id[2]) || !isalpha(chunk->id[3])) { + g_critical("SMF error: chunk signature contains at least one non-alphanumeric byte."); + return NULL; + } + + smf->next_chunk_offset += sizeof(struct chunk_header_struct) + ntohl(chunk->length); + + if (smf->next_chunk_offset > smf->file_buffer_length) { + g_critical("SMF error: malformed chunk; truncated file?"); + return NULL; + } + + return chunk; +} + +/** + * Returns 1, iff signature of the "chunk" is the same as string passed as "signature". + */ +static int +chunk_signature_matches(const struct chunk_header_struct *chunk, const char *signature) +{ + if (!memcmp(chunk->id, signature, 4)) + return 1; + + return 0; +} + +/** + * Verifies if MThd header looks OK. Returns 0 iff it does. + */ +static int +parse_mthd_header(smf_t *smf) +{ + int len; + struct chunk_header_struct *mthd, *tmp_mthd; + + /* Make sure compiler didn't do anything stupid. */ + assert(sizeof(struct chunk_header_struct) == 8); + + /* + * We could just do "mthd = smf->file_buffer;" here, but this way we wouldn't + * get useful error messages. + */ + if (smf->file_buffer_length < 6) { + g_critical("SMF error: file is too short, it cannot be a MIDI file."); + + return -1; + } + + tmp_mthd = smf->file_buffer; + + if (!chunk_signature_matches(tmp_mthd, "MThd")) { + g_critical("SMF error: MThd signature not found, is that a MIDI file?"); + + return -2; + } + + /* Ok, now use next_chunk(). */ + mthd = next_chunk(smf); + if (mthd == NULL) + return -3; + + assert(mthd == tmp_mthd); + + len = ntohl(mthd->length); + if (len != 6) { + g_critical("SMF error: MThd chunk length %d, must be 6.", len); + + return -4; + } + + return 0; +} + +/** + * Parses MThd chunk, filling "smf" structure with values extracted from it. Returns 0 iff everything went OK. + */ +static int +parse_mthd_chunk(smf_t *smf) +{ + signed char first_byte_of_division, second_byte_of_division; + + struct mthd_chunk_struct *mthd; + + assert(sizeof(struct mthd_chunk_struct) == 14); + + if (parse_mthd_header(smf)) + return 1; + + mthd = (struct mthd_chunk_struct *)smf->file_buffer; + + smf->format = ntohs(mthd->format); + if (smf->format < 0 || smf->format > 2) { + g_critical("SMF error: bad MThd format field value: %d, valid values are 0-2, inclusive.", smf->format); + return -1; + } + + if (smf->format == 2) { + g_critical("SMF file uses format #2, no support for that yet."); + return -2; + } + + smf->expected_number_of_tracks = ntohs(mthd->number_of_tracks); + if (smf->expected_number_of_tracks <= 0) { + g_critical("SMF error: bad number of tracks: %d, must be greater than zero.", smf->expected_number_of_tracks); + return -3; + } + + /* XXX: endianess? */ + first_byte_of_division = *((signed char *)&(mthd->division)); + second_byte_of_division = *((signed char *)&(mthd->division) + 1); + + if (first_byte_of_division >= 0) { + smf->ppqn = ntohs(mthd->division); + smf->frames_per_second = 0; + smf->resolution = 0; + } else { + smf->ppqn = 0; + smf->frames_per_second = - first_byte_of_division; + smf->resolution = second_byte_of_division; + } + + if (smf->ppqn == 0) { + g_critical("SMF file uses FPS timing instead of PPQN, no support for that yet."); + return -4; + } + + return 0; +} + +/** + * Interprets Variable Length Quantity pointed at by "buf" and puts its value into "value" and number + * of bytes consumed into "len", making sure it does not read past "buf" + "buffer_length". + * Explanation of Variable Length Quantities is here: http://www.borg.com/~jglatt/tech/midifile/vari.htm + * Returns 0 iff everything went OK, different value in case of error. + */ +static int +extract_vlq(const unsigned char *buf, const int buffer_length, int *value, int *len) +{ + int val = 0; + const unsigned char *c = buf; + + assert(buffer_length > 0); + + for (;;) { + if (c >= buf + buffer_length) { + g_critical("End of buffer in extract_vlq()."); + return -1; + } + + val = (val << 7) + (*c & 0x7F); + + if (*c & 0x80) + c++; + else + break; + }; + + *value = val; + *len = c - buf + 1; + + if (*len > 4) { + g_critical("SMF error: Variable Length Quantities longer than four bytes are not supported yet."); + return -2; + } + + return 0; +} + +/** + * Returns 1 if the given byte is a valid status byte, 0 otherwise. + */ +int +is_status_byte(const unsigned char status) +{ + return (status & 0x80); +} + +static int +is_sysex_byte(const unsigned char status) +{ + if (status == 0xF0) + return 1; + + return 0; +} + +static int +is_escape_byte(const unsigned char status) +{ + if (status == 0xF7) + return 1; + + return 0; +} + +/** + * Just like expected_message_length(), but only for System Exclusive messages. + * Note that value returned by this thing here is the length of SysEx "on the wire", + * not the number of bytes that this sysex takes in the file - in SMF format sysex + * contains VLQ telling how many bytes it takes, "on the wire" format does not have + * this. + */ +static int +expected_sysex_length(const unsigned char status, const unsigned char *second_byte, const int buffer_length, int *consumed_bytes) +{ + int sysex_length, len; + + assert(status == 0xF0); + + if (buffer_length < 3) { + g_critical("SMF error: end of buffer in expected_sysex_length()."); + return -1; + } + + extract_vlq(second_byte, buffer_length, &sysex_length, &len); + + if (consumed_bytes != NULL) + *consumed_bytes = len; + + /* +1, because the length does not include status byte. */ + return sysex_length + 1; +} + +static int +expected_escaped_length(const unsigned char status, const unsigned char *second_byte, const int buffer_length, int *consumed_bytes) +{ + /* -1, because we do not want to account for 0x7F status. */ + return expected_sysex_length(status, second_byte, buffer_length, consumed_bytes) - 1; +} + +/** + * Returns expected length of the midi message (including the status byte), in bytes, for the given status byte. + * The "second_byte" points to the expected second byte of the MIDI message. "buffer_length" is the buffer + * length limit, counting from "second_byte". Returns value < 0 iff there was an error. + */ +static int +expected_message_length(unsigned char status, const unsigned char *second_byte, const int buffer_length) +{ + /* Make sure this really is a valid status byte. */ + assert(is_status_byte(status)); + + /* We cannot use this routine for sysexes. */ + assert(!is_sysex_byte(status)); + + /* We cannot use this routine for escaped events. */ + assert(!is_escape_byte(status)); + + /* Buffer length may be zero, for e.g. realtime messages. */ + assert(buffer_length >= 0); + + /* Is this a metamessage? */ + if (status == 0xFF) { + if (buffer_length < 2) { + g_critical("SMF error: end of buffer in expected_message_length()."); + return -1; + } + + /* + * Format of this kind of messages is like this: 0xFF 0xwhatever 0xlength and then "length" bytes. + * Second byte points to this: ^^^^^^^^^^ + */ + return *(second_byte + 1) + 3; + } + + if ((status & 0xF0) == 0xF0) { + switch (status) { + case 0xF2: /* Song Position Pointer. */ + return 3; + + case 0xF1: /* MTC Quarter Frame. */ + case 0xF3: /* Song Select. */ + return 2; + + case 0xF6: /* Tune Request. */ + case 0xF8: /* MIDI Clock. */ + case 0xF9: /* Tick. */ + case 0xFA: /* MIDI Start. */ + case 0xFB: /* MIDI Continue. */ + case 0xFC: /* MIDI Stop. */ + case 0xFE: /* Active Sense. */ + return 1; + + default: + g_critical("SMF error: unknown 0xFx-type status byte '0x%x'.", status); + return -2; + } + } + + /* Filter out the channel. */ + status &= 0xF0; + + switch (status) { + case 0x80: /* Note Off. */ + case 0x90: /* Note On. */ + case 0xA0: /* AfterTouch. */ + case 0xB0: /* Control Change. */ + case 0xE0: /* Pitch Wheel. */ + return 3; + + case 0xC0: /* Program Change. */ + case 0xD0: /* Channel Pressure. */ + return 2; + + default: + g_critical("SMF error: unknown status byte '0x%x'.", status); + return -3; + } +} + +static int +extract_sysex_event(const unsigned char *buf, const int buffer_length, smf_event_t *event, int *len, int last_status) +{ + int status, message_length, vlq_length; + const unsigned char *c = buf; + + status = *buf; + + assert(is_sysex_byte(status)); + + c++; + + message_length = expected_sysex_length(status, c, buffer_length - 1, &vlq_length); + + if (message_length < 0) + return -3; + + c += vlq_length; + + if (vlq_length + message_length >= buffer_length) { + g_critical("End of buffer in extract_sysex_event()."); + return -5; + } + + event->midi_buffer_length = message_length; + event->midi_buffer = malloc(event->midi_buffer_length); + if (event->midi_buffer == NULL) { + g_critical("Cannot allocate memory in extract_sysex_event(): %s", strerror(errno)); + return -4; + } + + event->midi_buffer[0] = status; + memcpy(event->midi_buffer + 1, c, message_length - 1); + + *len = vlq_length + message_length; + + return 0; +} + +static int +extract_escaped_event(const unsigned char *buf, const int buffer_length, smf_event_t *event, int *len, int last_status) +{ + int status, message_length, vlq_length; + const unsigned char *c = buf; + + status = *buf; + + assert(is_escape_byte(status)); + + c++; + + message_length = expected_escaped_length(status, c, buffer_length - 1, &vlq_length); + + if (message_length < 0) + return -3; + + c += vlq_length; + + if (vlq_length + message_length >= buffer_length) { + g_critical("End of buffer in extract_escaped_event()."); + return -5; + } + + event->midi_buffer_length = message_length; + event->midi_buffer = malloc(event->midi_buffer_length); + if (event->midi_buffer == NULL) { + g_critical("Cannot allocate memory in extract_escaped_event(): %s", strerror(errno)); + return -4; + } + + memcpy(event->midi_buffer, c, message_length); + + if (smf_event_is_valid(event)) { + g_critical("Escaped event is invalid."); + return -1; + } + + if (smf_event_is_system_realtime(event) || smf_event_is_system_common(event)) { + g_warning("Escaped event is not system realtime nor system common."); + } + + *len = vlq_length + message_length; + + return 0; +} + + +/** + * Puts MIDI data extracted from from "buf" into "event" and number of consumed bytes into "len". + * In case valid status is not found, it uses "last_status" (so called "running status"). + * Returns 0 iff everything went OK, value < 0 in case of error. + */ +static int +extract_midi_event(const unsigned char *buf, const int buffer_length, smf_event_t *event, int *len, int last_status) +{ + int status, message_length; + const unsigned char *c = buf; + + assert(buffer_length > 0); + + /* Is the first byte the status byte? */ + if (is_status_byte(*c)) { + status = *c; + c++; + + } else { + /* No, we use running status then. */ + status = last_status; + } + + if (!is_status_byte(status)) { + g_critical("SMF error: bad status byte (MSB is zero)."); + return -1; + } + + if (is_sysex_byte(status)) + return extract_sysex_event(buf, buffer_length, event, len, last_status); + + if (is_escape_byte(status)) + return extract_escaped_event(buf, buffer_length, event, len, last_status); + + /* At this point, "c" points to first byte following the status byte. */ + message_length = expected_message_length(status, c, buffer_length - (c - buf)); + + if (message_length < 0) + return -3; + + if (message_length - 1 > buffer_length - (c - buf)) { + g_critical("End of buffer in extract_midi_event()."); + return -5; + } + + event->midi_buffer_length = message_length; + event->midi_buffer = malloc(event->midi_buffer_length); + if (event->midi_buffer == NULL) { + g_critical("Cannot allocate memory in extract_midi_event(): %s", strerror(errno)); + return -4; + } + + event->midi_buffer[0] = status; + memcpy(event->midi_buffer + 1, c, message_length - 1); + + *len = c + message_length - 1 - buf; + + return 0; +} + +/** + * Locates, basing on track->next_event_offset, the next event data in track->buffer, + * interprets it, allocates smf_event_t and fills it properly. Returns smf_event_t + * or NULL, if there was an error. Allocating event means adding it to the track; + * see smf_event_new(). + */ +static smf_event_t * +parse_next_event(smf_track_t *track) +{ + int time = 0, len, buffer_length; + unsigned char *c, *start; + + smf_event_t *event = smf_event_new(); + if (event == NULL) + goto error; + + c = start = (unsigned char *)track->file_buffer + track->next_event_offset; + + assert(track->file_buffer != NULL); + assert(track->file_buffer_length > 0); + assert(track->next_event_offset > 0); + + buffer_length = track->file_buffer_length - track->next_event_offset; + assert(buffer_length > 0); + + /* First, extract time offset from previous event. */ + if (extract_vlq(c, buffer_length, &time, &len)) + goto error; + + c += len; + buffer_length -= len; + + if (buffer_length <= 0) + goto error; + + /* Now, extract the actual event. */ + if (extract_midi_event(c, buffer_length, event, &len, track->last_status)) + goto error; + + c += len; + buffer_length -= len; + track->last_status = event->midi_buffer[0]; + track->next_event_offset += c - start; + + smf_track_add_event_delta_pulses(track, event, time); + + return event; + +error: + if (event != NULL) + smf_event_delete(event); + + return NULL; +} + +/** + * Takes "len" characters starting in "buf", making sure it does not access past the length of the buffer, + * and makes ordinary, zero-terminated string from it. May return NULL if there was any problem. + */ +static char * +make_string(const unsigned char *buf, const int buffer_length, int len) +{ + char *str; + + assert(buffer_length > 0); + assert(len > 0); + + if (len > buffer_length) { + g_critical("End of buffer in make_string()."); + + len = buffer_length; + } + + str = malloc(len + 1); + if (str == NULL) { + g_critical("Cannot allocate memory in make_string()."); + return NULL; + } + + memcpy(str, buf, len); + str[len] = '\0'; + + return str; +} + +/** + * Returns zero-terminated string extracted from "text events" or NULL, if there was any problem. + */ +char * +smf_string_from_event(const smf_event_t *event) +{ + int string_length = -1, length_length = -1; + + if (event->midi_buffer_length < 3) { + g_critical("smf_string_from_event: truncated MIDI message."); + return NULL; + } + + extract_vlq((void *)&(event->midi_buffer[2]), event->midi_buffer_length - 2, &string_length, &length_length); + + if (string_length <= 0) { + g_critical("smf_string_from_event: truncated MIDI message."); + return NULL; + } + + return make_string((void *)(&event->midi_buffer[2] + length_length), event->midi_buffer_length - 2 - length_length, string_length); +} + +/** + * Verify if the next chunk really is MTrk chunk, and if so, initialize some track variables and return 0. + * Return different value otherwise. + */ +static int +parse_mtrk_header(smf_track_t *track) +{ + struct chunk_header_struct *mtrk; + + /* Make sure compiler didn't do anything stupid. */ + assert(sizeof(struct chunk_header_struct) == 8); + assert(track->smf != NULL); + + mtrk = next_chunk(track->smf); + + if (mtrk == NULL) + return -1; + + if (!chunk_signature_matches(mtrk, "MTrk")) { + g_warning("SMF warning: Expected MTrk signature, got %c%c%c%c instead; ignoring this chunk.", + mtrk->id[0], mtrk->id[1], mtrk->id[2], mtrk->id[3]); + + return -2; + } + + track->file_buffer = mtrk; + track->file_buffer_length = sizeof(struct chunk_header_struct) + ntohl(mtrk->length); + track->next_event_offset = sizeof(struct chunk_header_struct); + + return 0; +} + +/** + * Return 1 if event is end-of-the-track, 0 otherwise. + */ +static int +event_is_end_of_track(const smf_event_t *event) +{ + if (event->midi_buffer[0] == 0xFF && event->midi_buffer[1] == 0x2F) + return 1; + + return 0; +} + +/** + * \return Nonzero, if event is as long as it should be, from the MIDI specification point of view. + * Does not work for SysExes - it doesn't recognize internal structure of SysEx. + */ +int +smf_event_length_is_valid(const smf_event_t *event) +{ + assert(event); + assert(event->midi_buffer); + + if (event->midi_buffer_length < 1) + return 0; + + /* We cannot use expected_message_length on sysexes. */ + if (smf_event_is_sysex(event)) + return 1; + + if (event->midi_buffer_length != expected_message_length(event->midi_buffer[0], + &(event->midi_buffer[1]), event->midi_buffer_length - 1)) { + + return 0; + } + + return 1; +} + +/** + * \return Nonzero, if MIDI data in the event is valid, 0 otherwise. For example, + * it checks if event length is correct. + */ +/* XXX: this routine requires some more work to detect more errors. */ +int +smf_event_is_valid(const smf_event_t *event) +{ + assert(event); + assert(event->midi_buffer); + assert(event->midi_buffer_length >= 1); + + if (!is_status_byte(event->midi_buffer[0])) { + g_critical("First byte of MIDI message is not a valid status byte."); + + return 0; + } + + if (!smf_event_length_is_valid(event)) + return 0; + + return 1; +} + +/** + * Parse events and put it on the track. + */ +static int +parse_mtrk_chunk(smf_track_t *track) +{ + smf_event_t *event; + + if (parse_mtrk_header(track)) + return -1; + + for (;;) { + event = parse_next_event(track); + + /* Couldn't parse an event? */ + if (event == NULL) + return -1; + + assert(smf_event_is_valid(event)); + + if (event_is_end_of_track(event)) + break; + } + + track->file_buffer = NULL; + track->file_buffer_length = 0; + track->next_event_offset = -1; + + return 0; +} + +/** + * Allocate buffer of proper size and read file contents into it. Close file afterwards. + */ +static int +load_file_into_buffer(void **file_buffer, int *file_buffer_length, const char *file_name) +{ + FILE *stream = fopen(file_name, "r"); + + if (stream == NULL) { + g_critical("Cannot open input file: %s", strerror(errno)); + + return -1; + } + + if (fseek(stream, 0, SEEK_END)) { + g_critical("fseek(3) failed: %s", strerror(errno)); + + return -2; + } + + *file_buffer_length = ftell(stream); + if (*file_buffer_length == -1) { + g_critical("ftell(3) failed: %s", strerror(errno)); + + return -3; + } + + if (fseek(stream, 0, SEEK_SET)) { + g_critical("fseek(3) failed: %s", strerror(errno)); + + return -4; + } + + *file_buffer = malloc(*file_buffer_length); + if (*file_buffer == NULL) { + g_critical("malloc(3) failed: %s", strerror(errno)); + + return -5; + } + + if (fread(*file_buffer, 1, *file_buffer_length, stream) != *file_buffer_length) { + g_critical("fread(3) failed: %s", strerror(errno)); + + return -6; + } + + if (fclose(stream)) { + g_critical("fclose(3) failed: %s", strerror(errno)); + + return -7; + } + + return 0; +} + +/** + * Creates new SMF and fills it with data loaded from the given buffer. + * \return SMF or NULL, if loading failed. + */ +smf_t * +smf_load_from_memory(const void *buffer, const int buffer_length) +{ + int i; + + smf_t *smf = smf_new(); + + smf->file_buffer = (void *)buffer; + smf->file_buffer_length = buffer_length; + smf->next_chunk_offset = 0; + + if (parse_mthd_chunk(smf)) + return NULL; + + for (i = 1; i <= smf->expected_number_of_tracks; i++) { + smf_track_t *track = smf_track_new(); + if (track == NULL) + return NULL; + + smf_add_track(smf, track); + + /* Skip unparseable chunks. */ + if (parse_mtrk_chunk(track)) { + g_warning("SMF warning: Cannot load track."); + smf_track_delete(track); + } + + track->file_buffer = NULL; + track->file_buffer_length = 0; + track->next_event_offset = -1; + } + + if (smf->expected_number_of_tracks != smf->number_of_tracks) { + g_warning("SMF warning: MThd header declared %d tracks, but only %d found; continuing anyway.", + smf->expected_number_of_tracks, smf->number_of_tracks); + + smf->expected_number_of_tracks = smf->number_of_tracks; + } + + smf->file_buffer = NULL; + smf->file_buffer_length = 0; + smf->next_chunk_offset = -1; + + return smf; +} + +/** + * Loads SMF file. + * \param file_name Path to the file. + * \return SMF or NULL, if loading failed. + */ +smf_t * +smf_load(const char *file_name) +{ + int file_buffer_length; + void *file_buffer; + smf_t *smf; + + if (load_file_into_buffer(&file_buffer, &file_buffer_length, file_name)) + return NULL; + + smf = smf_load_from_memory(file_buffer, file_buffer_length); + + free(file_buffer); + + if (smf == NULL) + return NULL; + + smf_rewind(smf); + + return smf; +} + diff --git a/libsmf/smf_private.h b/libsmf/smf_private.h new file mode 100644 index 0000000..79a6924 --- /dev/null +++ b/libsmf/smf_private.h @@ -0,0 +1,60 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * ALTHOUGH THIS SOFTWARE IS MADE OF WIN AND SCIENCE, IT IS PROVIDED BY THE + * AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +#ifndef SMF_PRIVATE_H +#define SMF_PRIVATE_H + +#include "config.h" + +#define SMF_VERSION PACKAGE_VERSION + +/* Structures used in smf_load.c and smf_save.c. */ +struct chunk_header_struct { + char id[4]; + uint32_t length; +} __attribute__((__packed__)); + +struct mthd_chunk_struct { + struct chunk_header_struct mthd_header; + uint16_t format; + uint16_t number_of_tracks; + uint16_t division; +} __attribute__((__packed__)); + +void smf_track_add_event(smf_track_t *track, smf_event_t *event); + +int smf_init_tempo(smf_t *smf); +int smf_create_tempo_map_and_compute_seconds(smf_t *smf); +void maybe_add_to_tempo_map(smf_event_t *event); +void remove_last_tempo_with_pulses(smf_t *smf, int pulses); +int smf_event_is_tempo_change_or_time_signature(const smf_event_t *event); +int smf_event_length_is_valid(const smf_event_t *event); +int smf_event_is_sysex(const smf_event_t *event); +int is_status_byte(const unsigned char status); + +#endif /* SMF_PRIVATE_H */ + diff --git a/libsmf/smf_save.c b/libsmf/smf_save.c new file mode 100644 index 0000000..ae77d69 --- /dev/null +++ b/libsmf/smf_save.c @@ -0,0 +1,523 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * ALTHOUGH THIS SOFTWARE IS MADE OF WIN AND SCIENCE, IT IS PROVIDED BY THE + * AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +/* + * This is Standard MIDI File writer. + * + * For questions and comments, contact Edward Tomasz Napierala . + */ + +/* Reference: http://www.borg.com/~jglatt/tech/midifile.htm */ + +#include +#include +#include +#include +#include +#include +#include "smf.h" +#include "smf_private.h" + +/** + * Extends (reallocates) smf->file_buffer and returns pointer to the newly added space, + * that is, pointer to the first byte after the previous buffer end. Returns NULL in case + * of error. + */ +static void * +smf_extend(smf_t *smf, const int length) +{ + int i, previous_file_buffer_length = smf->file_buffer_length; + char *previous_file_buffer = smf->file_buffer; + + /* XXX: Not terribly efficient. */ + smf->file_buffer_length += length; + smf->file_buffer = realloc(smf->file_buffer, smf->file_buffer_length); + if (smf->file_buffer == NULL) { + g_critical("realloc(3) failed: %s", strerror(errno)); + smf->file_buffer_length = 0; + return NULL; + } + + /* Fix up pointers. XXX: omgwtf. */ + for (i = 1; i <= smf->number_of_tracks; i++) { + smf_track_t *track; + track = smf_get_track_by_number(smf, i); + if (track->file_buffer != NULL) + track->file_buffer = (char *)track->file_buffer + ((char *)smf->file_buffer - previous_file_buffer); + } + + return (char *)smf->file_buffer + previous_file_buffer_length; +} + +/** + * Appends "buffer_length" bytes pointed to by "buffer" to the smf, reallocating storage as needed. Returns 0 + * if everything went ok, different value if there was any problem. + */ +static int +smf_append(smf_t *smf, const void *buffer, const int buffer_length) +{ + void *dest; + + dest = smf_extend(smf, buffer_length); + if (dest == NULL) { + g_critical("Cannot extend track buffer."); + return -1; + } + + memcpy(dest, buffer, buffer_length); + + return 0; +} + +/** + * Appends MThd header to the track. Returns 0 if everything went ok, different value if not. + */ +static int +write_mthd_header(smf_t *smf) +{ + struct mthd_chunk_struct mthd_chunk; + + memcpy(mthd_chunk.mthd_header.id, "MThd", 4); + mthd_chunk.mthd_header.length = htonl(6); + mthd_chunk.format = htons(smf->format); + mthd_chunk.number_of_tracks = htons(smf->number_of_tracks); + mthd_chunk.division = htons(smf->ppqn); + + return smf_append(smf, &mthd_chunk, sizeof(mthd_chunk)); +} + +/** + * Extends (reallocates) track->file_buffer and returns pointer to the newly added space, + * that is, pointer to the first byte after the previous buffer end. Returns NULL in case + * of error. + */ +static void * +track_extend(smf_track_t *track, const int length) +{ + void *buf; + + assert(track->smf); + + buf = smf_extend(track->smf, length); + if (buf == NULL) + return NULL; + + track->file_buffer_length += length; + if (track->file_buffer == NULL) + track->file_buffer = buf; + + return buf; +} + +/** + * Appends "buffer_length" bytes pointed to by "buffer" to the track, reallocating storage as needed. Returns 0 + * if everything went ok, different value if there was any problem. + */ +static int +track_append(smf_track_t *track, const void *buffer, const int buffer_length) +{ + void *dest; + + dest = track_extend(track, buffer_length); + if (dest == NULL) { + g_critical("Cannot extend track buffer."); + return -1; + } + + memcpy(dest, buffer, buffer_length); + + return 0; +} + +/** + * Appends value, expressed as Variable Length Quantity, to event->track. + */ +static int +write_vlq(smf_event_t *event, unsigned long value) +{ + unsigned long buffer; + int ret; + + assert(event->track); + + /* Taken from http://www.borg.com/~jglatt/tech/midifile/vari.htm */ + buffer = value & 0x7F; + + while ((value >>= 7)) { + buffer <<= 8; + buffer |= ((value & 0x7F) | 0x80); + } + + for (;;) { + ret = track_append(event->track, &buffer, 1); + if (ret) + return ret; + + if (buffer & 0x80) + buffer >>= 8; + else + break; + } + + return 0; +} + +/** + * Appends event time as Variable Length Quantity. Returns 0 if everything went ok, + * different value in case of error. + */ +static int +write_event_time(smf_event_t *event) +{ + assert(event->delta_time_pulses >= 0); + + return write_vlq(event, event->delta_time_pulses); +} + +static int +write_sysex_contents(smf_event_t *event) +{ + int ret; + unsigned char sysex_status = 0xF0; + + assert(smf_event_is_sysex(event)); + + ret = track_append(event->track, &sysex_status, 1); + if (ret) + return ret; + + /* -1, because length does not include status byte. */ + ret = write_vlq(event, event->midi_buffer_length - 1); + if (ret) + return ret; + + ret = track_append(event->track, event->midi_buffer + 1, event->midi_buffer_length - 1); + if (ret) + return ret; + + return 0; +} + +/** + * Appends contents of event->midi_buffer wrapped into 0xF7 MIDI event. + */ +static int +write_escaped_event_contents(smf_event_t *event) +{ + int ret; + unsigned char escape_status = 0xF7; + + if (smf_event_is_sysex(event)) + return write_sysex_contents(event); + + ret = track_append(event->track, &escape_status, 1); + if (ret) + return ret; + + ret = write_vlq(event, event->midi_buffer_length); + if (ret) + return ret; + + ret = track_append(event->track, event->midi_buffer, event->midi_buffer_length); + if (ret) + return ret; + + return 0; +} + +/** + * Appends contents of event->midi_buffer. Returns 0 if everything went 0, + * different value in case of error. + */ +static int +write_event_contents(smf_event_t *event) +{ + if (smf_event_is_system_realtime(event) || smf_event_is_system_common(event)) + return write_escaped_event_contents(event); + + return track_append(event->track, event->midi_buffer, event->midi_buffer_length); +} + +/** + * Writes out an event. + */ +static int +write_event(smf_event_t *event) +{ + int ret; + + ret = write_event_time(event); + if (ret) + return ret; + + ret = write_event_contents(event); + if (ret) + return ret; + + return 0; +} + +/** + * Writes out MTrk header, except of MTrk chunk length, which is written by write_mtrk_length(). + */ +static int +write_mtrk_header(smf_track_t *track) +{ + struct chunk_header_struct mtrk_header; + + memcpy(mtrk_header.id, "MTrk", 4); + + return track_append(track, &mtrk_header, sizeof(mtrk_header)); +} + +/** + * Updates MTrk chunk length of a given track. + */ +static int +write_mtrk_length(smf_track_t *track) +{ + struct chunk_header_struct *mtrk_header; + + assert(track->file_buffer != NULL); + assert(track->file_buffer_length >= 6); + + mtrk_header = (struct chunk_header_struct *)track->file_buffer; + mtrk_header->length = htonl(track->file_buffer_length - sizeof(struct chunk_header_struct)); + + return 0; +} + +/** + * Writes out the track. + */ +static int +write_track(smf_track_t *track) +{ + int ret; + smf_event_t *event; + + ret = write_mtrk_header(track); + if (ret) + return ret; + + while ((event = smf_track_get_next_event(track)) != NULL) { + ret = write_event(event); + if (ret) + return ret; + } + + ret = write_mtrk_length(track); + if (ret) + return ret; + + return 0; +} + +/** + * Takes smf->file_buffer and saves it to the file. Frees the buffer afterwards. + */ +static int +write_file_and_free_buffer(smf_t *smf, const char *file_name) +{ + int i; + FILE *stream; + smf_track_t *track; + + stream = fopen(file_name, "w+"); + if (stream == NULL) { + g_critical("Cannot open input file: %s", strerror(errno)); + + return -1; + } + + if (fwrite(smf->file_buffer, 1, smf->file_buffer_length, stream) != smf->file_buffer_length) { + g_critical("fwrite(3) failed: %s", strerror(errno)); + + return -2; + } + + if (fclose(stream)) { + g_critical("fclose(3) failed: %s", strerror(errno)); + + return -3; + } + + /* Clear the pointers. */ + free(smf->file_buffer); + smf->file_buffer = NULL; + smf->file_buffer_length = 0; + + for (i = 1; i <= smf->number_of_tracks; i++) { + track = smf_get_track_by_number(smf, i); + assert(track); + track->file_buffer = NULL; + track->file_buffer_length = 0; + } + + return 0; +} + +/** + * \return Nonzero, if all pointers supposed to be NULL are NULL. Triggers assertion if not. + */ +static int +pointers_are_clear(smf_t *smf) +{ + int i; + + smf_track_t *track; + assert(smf->file_buffer == NULL); + assert(smf->file_buffer_length == 0); + + for (i = 1; i <= smf->number_of_tracks; i++) { + track = smf_get_track_by_number(smf, i); + + assert(track != NULL); + assert(track->file_buffer == NULL); + assert(track->file_buffer_length == 0); + } + + return 1; +} + +/** + * \return Nonzero, if event is End Of Track. + */ +int +smf_event_is_eot(const smf_event_t *event) +{ + if (event->midi_buffer_length != 3) + return 0; + + if (event->midi_buffer[0] != 0xFF || event->midi_buffer[1] != 0x2F || event->midi_buffer[2] != 0x00) + return 0; + + return 1; +} + +/** + * Check if SMF is valid and add missing EOT events. + * + * \return 0, if SMF is valid. + */ +static int +smf_validate(smf_t *smf) +{ + int trackno, eventno, eot_found; + smf_track_t *track; + smf_event_t *event; + + if (smf->format < 0 || smf->format > 2) { + g_critical("SMF error: smf->format is less than zero of greater than two."); + return -1; + } + + if (smf->number_of_tracks < 1) { + g_critical("SMF error: number of tracks is less than one."); + return -2; + } + + if (smf->format == 0 && smf->number_of_tracks > 1) { + g_critical("SMF error: format is 0, but number of tracks is more than one."); + return -3; + } + + if (smf->ppqn <= 0) { + g_critical("SMF error: PPQN has to be > 0."); + return -4; + } + + for (trackno = 1; trackno <= smf->number_of_tracks; trackno++) { + track = smf_get_track_by_number(smf, trackno); + assert(track); + + eot_found = 0; + + for (eventno = 1; eventno <= track->number_of_events; eventno++) { + event = smf_track_get_event_by_number(track, eventno); + assert(event); + + if (!smf_event_is_valid(event)) { + g_critical("Event #%d on track #%d is invalid.", eventno, trackno); + return -5; + } + + if (smf_event_is_eot(event)) { + if (eot_found) { + g_critical("Duplicate End Of Track event on track #%d.", trackno); + return -6; + } + + eot_found = 1; + } + } + + if (!eot_found) + smf_track_add_eot_delta_pulses(track, 0); + } + + return 0; +} + +/** + * Writes the contents of SMF to the file given. + * \param smf SMF. + * \param file_name Path to the file. + * \return 0, if saving was successfull. + */ +int +smf_save(smf_t *smf, const char *file_name) +{ + int i, ret; + smf_track_t *track; + + smf_rewind(smf); + + assert(pointers_are_clear(smf)); + + if (smf_validate(smf)) + return -1; + + if (write_mthd_header(smf)) + return -2; + + for (i = 1; i <= smf->number_of_tracks; i++) { + track = smf_get_track_by_number(smf, i); + + assert(track != NULL); + + ret = write_track(track); + if (ret) + return ret; + } + + if (write_file_and_free_buffer(smf, file_name)) + return -3; + + return 0; +} + diff --git a/libsmf/smf_tempo.c b/libsmf/smf_tempo.c new file mode 100644 index 0000000..8431c7d --- /dev/null +++ b/libsmf/smf_tempo.c @@ -0,0 +1,421 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * ALTHOUGH THIS SOFTWARE IS MADE OF WIN AND SCIENCE, IT IS PROVIDED BY THE + * AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +/* + * This is Standard MIDI File format implementation, tempo map related part. + * + * For questions and comments, contact Edward Tomasz Napierala . + */ + +#include +#include +#include +#include "smf.h" +#include "smf_private.h" + +static double seconds_from_pulses(const smf_t *smf, int pulses); + +/** + * If there is tempo starting at "pulses" already, return it. Otherwise, + * allocate new one, fill it with values from previous one (or default ones, + * if there is no previous one) and attach it to "smf". + */ +static smf_tempo_t * +new_tempo(smf_t *smf, int pulses) +{ + smf_tempo_t *tempo, *previous_tempo = NULL; + + if (smf->tempo_array->len > 0) { + previous_tempo = smf_get_last_tempo(smf); + + /* If previous tempo starts at the same time as new one, reuse it, updating in place. */ + if (previous_tempo->time_pulses == pulses) + return previous_tempo; + } + + tempo = malloc(sizeof(smf_tempo_t)); + if (tempo == NULL) { + g_critical("Cannot allocate smf_tempo_t."); + return NULL; + } + + tempo->time_pulses = pulses; + + if (previous_tempo != NULL) { + tempo->microseconds_per_quarter_note = previous_tempo->microseconds_per_quarter_note; + tempo->numerator = previous_tempo->numerator; + tempo->denominator = previous_tempo->denominator; + tempo->clocks_per_click = previous_tempo->clocks_per_click; + tempo->notes_per_note = previous_tempo->notes_per_note; + } else { + tempo->microseconds_per_quarter_note = 500000; /* Initial tempo is 120 BPM. */ + tempo->numerator = 4; + tempo->denominator = 4; + tempo->clocks_per_click = -1; + tempo->notes_per_note = -1; + } + + g_ptr_array_add(smf->tempo_array, tempo); + + if (pulses == 0) + tempo->time_seconds = 0.0; + else + tempo->time_seconds = seconds_from_pulses(smf, pulses); + + return tempo; +} + +static int +add_tempo(smf_t *smf, int pulses, int tempo) +{ + smf_tempo_t *smf_tempo = new_tempo(smf, pulses); + if (smf_tempo == NULL) + return -1; + + smf_tempo->microseconds_per_quarter_note = tempo; + + return 0; +} + +static int +add_time_signature(smf_t *smf, int pulses, int numerator, int denominator, int clocks_per_click, int notes_per_note) +{ + smf_tempo_t *smf_tempo = new_tempo(smf, pulses); + if (smf_tempo == NULL) + return -1; + + smf_tempo->numerator = numerator; + smf_tempo->denominator = denominator; + smf_tempo->clocks_per_click = clocks_per_click; + smf_tempo->notes_per_note = notes_per_note; + + return 0; +} + +void +maybe_add_to_tempo_map(smf_event_t *event) +{ + if (!smf_event_is_metadata(event)) + return; + + assert(event->track != NULL); + assert(event->track->smf != NULL); + assert(event->midi_buffer_length >= 1); + + /* Tempo Change? */ + if (event->midi_buffer[1] == 0x51) { + int new_tempo = (event->midi_buffer[3] << 16) + (event->midi_buffer[4] << 8) + event->midi_buffer[5]; + if (new_tempo <= 0) { + g_critical("Ignoring invalid tempo change."); + return; + } + + add_tempo(event->track->smf, event->time_pulses, new_tempo); + } + + /* Time Signature? */ + if (event->midi_buffer[1] == 0x58) { + int numerator, denominator, clocks_per_click, notes_per_note; + + if (event->midi_buffer_length < 7) { + g_critical("Time Signature event seems truncated."); + return; + } + + numerator = event->midi_buffer[3]; + denominator = (int)pow(2, event->midi_buffer[4]); + clocks_per_click = event->midi_buffer[5]; + notes_per_note = event->midi_buffer[6]; + + add_time_signature(event->track->smf, event->time_pulses, numerator, denominator, clocks_per_click, notes_per_note); + } + + return; +} + +/** + * This is an internal function, called from smf_track_remove_event when tempo-related + * event being removed does not require recreation of tempo map, i.e. there are no events + * after that one. + */ +void +remove_last_tempo_with_pulses(smf_t *smf, int pulses) +{ + smf_tempo_t *tempo; + + /* XXX: This is a workaround for the following problem: we have two tempo-related + events, A and B, that occur at the same time. We remove B, then try to remove + A. However, both tempo changes got coalesced in new_tempo(), so it is impossible + to remove B. */ + if (smf->tempo_array->len == 0) + return; + + tempo = smf_get_last_tempo(smf); + + /* Workaround part two. */ + if (tempo->time_pulses != pulses) + return; + + free(tempo); + + g_ptr_array_remove_index(smf->tempo_array, smf->tempo_array->len - 1); +} + +static double +seconds_from_pulses(const smf_t *smf, int pulses) +{ + double seconds; + smf_tempo_t *tempo; + + tempo = smf_get_tempo_by_pulses(smf, pulses); + assert(tempo); + assert(tempo->time_pulses <= pulses); + + seconds = tempo->time_seconds + (double)(pulses - tempo->time_pulses) * + (tempo->microseconds_per_quarter_note / ((double)smf->ppqn * 1000000.0)); + + return seconds; +} + +static int +pulses_from_seconds(const smf_t *smf, double seconds) +{ + int pulses = 0; + smf_tempo_t *tempo; + + tempo = smf_get_tempo_by_seconds(smf, seconds); + assert(tempo); + assert(tempo->time_seconds <= seconds); + + pulses = tempo->time_pulses + (seconds - tempo->time_seconds) * + ((double)smf->ppqn * 1000000.0 / tempo->microseconds_per_quarter_note); + + return pulses; +} + +/** + * Computes value of event->time_seconds for all events in smf. + * Warning: rewinds the smf. + */ +int +smf_create_tempo_map_and_compute_seconds(smf_t *smf) +{ + smf_event_t *event; + + smf_rewind(smf); + smf_init_tempo(smf); + + for (;;) { + event = smf_get_next_event(smf); + + if (event == NULL) + return 0; + + maybe_add_to_tempo_map(event); + + event->time_seconds = seconds_from_pulses(smf, event->time_pulses); + } + + /* Not reached. */ + return -1; +} + +smf_tempo_t * +smf_get_tempo_by_number(const smf_t *smf, int number) +{ + assert(number >= 0); + + if (number >= smf->tempo_array->len) + return NULL; + + return g_ptr_array_index(smf->tempo_array, number); +} + +/** + * Return last tempo (i.e. tempo with greatest time_pulses) that happens before "pulses". + */ +smf_tempo_t * +smf_get_tempo_by_pulses(const smf_t *smf, int pulses) +{ + int i; + smf_tempo_t *tempo; + + assert(pulses >= 0); + + if (pulses == 0) + return smf_get_tempo_by_number(smf, 0); + + assert(smf->tempo_array != NULL); + + for (i = smf->tempo_array->len - 1; i >= 0; i--) { + tempo = smf_get_tempo_by_number(smf, i); + + assert(tempo); + if (tempo->time_pulses < pulses) + return tempo; + } + + return NULL; +} + +/** + * Return last tempo (i.e. tempo with greatest time_seconds) that happens before "seconds". + */ +smf_tempo_t * +smf_get_tempo_by_seconds(const smf_t *smf, double seconds) +{ + int i; + smf_tempo_t *tempo; + + assert(seconds >= 0.0); + + if (seconds == 0.0) + return smf_get_tempo_by_number(smf, 0); + + assert(smf->tempo_array != NULL); + + for (i = smf->tempo_array->len - 1; i >= 0; i--) { + tempo = smf_get_tempo_by_number(smf, i); + + assert(tempo); + if (tempo->time_seconds < seconds) + return tempo; + } + + return NULL; +} + + +/** + * Return last tempo. + */ +smf_tempo_t * +smf_get_last_tempo(const smf_t *smf) +{ + smf_tempo_t *tempo; + + tempo = smf_get_tempo_by_number(smf, smf->tempo_array->len - 1); + assert(tempo); + + return tempo; +} + +/** + * Remove any existing tempos and add default one. + */ +int +smf_init_tempo(smf_t *smf) +{ + smf_tempo_t *tempo; + + while (smf->tempo_array->len > 0) { + smf_tempo_t *tempo = g_ptr_array_index(smf->tempo_array, smf->tempo_array->len - 1); + assert(tempo); + free(tempo); + g_ptr_array_remove_index(smf->tempo_array, smf->tempo_array->len - 1); + } + + assert(smf->tempo_array->len == 0); + + tempo = new_tempo(smf, 0); + if (tempo == NULL) + return -1; + + return 0; +} + +/** + * Returns ->time_pulses of last event on the given track, or 0, if track is empty. + */ +static int +last_event_pulses(const smf_track_t *track) +{ + /* Get time of last event on this track. */ + if (track->number_of_events > 0) { + smf_event_t *previous_event = smf_track_get_last_event(track); + assert(previous_event); + assert(previous_event->time_pulses >= 0); + + return previous_event->time_pulses; + } + + return 0; +} + +/** + * Adds event to the track at the time "pulses" clocks from the previous event in this track. + * The remaining two time fields will be computed automatically based on the third argument + * and current tempo map. Note that ->delta_pulses is computed by smf.c:smf_track_add_event, + * not here. + */ +void +smf_track_add_event_delta_pulses(smf_track_t *track, smf_event_t *event, int delta) +{ + assert(delta >= 0); + assert(event->time_pulses == -1); + assert(event->time_seconds == -1.0); + assert(track->smf != NULL); + + smf_track_add_event_pulses(track, event, last_event_pulses(track) + delta); +} + +/** + * Adds event to the track at the time "pulses" clocks from the start of song. + * The remaining two time fields will be computed automatically based on the third argument + * and current tempo map. + */ +void +smf_track_add_event_pulses(smf_track_t *track, smf_event_t *event, int pulses) +{ + assert(pulses >= 0); + assert(event->time_pulses == -1); + assert(event->time_seconds == -1.0); + assert(track->smf != NULL); + + event->time_pulses = pulses; + event->time_seconds = seconds_from_pulses(track->smf, pulses); + smf_track_add_event(track, event); +} + +/** + * Adds event to the track at the time "seconds" seconds from the start of song. + * The remaining two time fields will be computed automatically based on the third argument + * and current tempo map. + */ +void +smf_track_add_event_seconds(smf_track_t *track, smf_event_t *event, double seconds) +{ + assert(seconds >= 0.0); + assert(event->time_pulses == -1); + assert(event->time_seconds == -1.0); + assert(track->smf != NULL); + + event->time_seconds = seconds; + event->time_pulses = pulses_from_seconds(track->smf, seconds); + smf_track_add_event(track, event); +} + diff --git a/libsmf/smfsh.c b/libsmf/smfsh.c new file mode 100644 index 0000000..c7d41d6 --- /dev/null +++ b/libsmf/smfsh.c @@ -0,0 +1,879 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * ALTHOUGH THIS SOFTWARE IS MADE OF WIN AND SCIENCE, IT IS PROVIDED BY THE + * AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +#include +#include +#include +#include +#include +#include "smf.h" +#include "config.h" + +#ifdef HAVE_LIBREADLINE +#include +#include +#endif + +smf_track_t *selected_track = NULL; +smf_event_t *selected_event = NULL; +smf_t *smf = NULL; +char *last_file_name = NULL; + +static void +usage(void) +{ + fprintf(stderr, "usage: smfsh [file]\n"); + + exit(EX_USAGE); +} + +static void +log_handler(const gchar *log_domain, GLogLevelFlags log_level, const gchar *message, gpointer notused) +{ + fprintf(stderr, "%s: %s\n", log_domain, message); +} + +static int cmd_track(char *arg); + +static int +cmd_load(char *file_name) +{ + if (file_name == NULL) { + if (last_file_name == NULL) { + g_critical("Please specify file name."); + return -1; + } + + file_name = last_file_name; + } + + if (smf != NULL) + smf_delete(smf); + + selected_track = NULL; + selected_event = NULL; + + last_file_name = strdup(file_name); + smf = smf_load(file_name); + if (smf == NULL) { + g_critical("Couldn't load '%s'.", file_name); + + smf = smf_new(); + if (smf == NULL) { + g_critical("Cannot initialize smf_t."); + return -1; + } + + return -2; + } + + g_message("File '%s' loaded.", file_name); + g_message("%s.", smf_decode(smf)); + + cmd_track("1"); + + return 0; +} + +static int +cmd_save(char *file_name) +{ + int ret; + + if (file_name == NULL) { + if (last_file_name == NULL) { + g_critical("Please specify file name."); + return -1; + } + + file_name = last_file_name; + } + + if (file_name == NULL) { + g_critical("Please specify file name."); + return -1; + } + + last_file_name = strdup(file_name); + ret = smf_save(smf, file_name); + if (ret) { + g_critical("Couldn't save '%s'", file_name); + return -1; + } + + g_message("File '%s' saved.", file_name); + + return 0; +} + +static int +cmd_ppqn(char *new_ppqn) +{ + int tmp; + char *end; + + if (new_ppqn == NULL) { + g_message("Pulses Per Quarter Note (aka Division) is %d.", smf->ppqn); + } else { + tmp = strtol(new_ppqn, &end, 10); + if (end - new_ppqn != strlen(new_ppqn)) { + g_critical("Invalid PPQN, garbage characters after the number."); + return -1; + } + + if (tmp <= 0) { + g_critical("Invalid PPQN, valid values are greater than zero."); + return -2; + } + + if (smf_set_ppqn(smf, tmp)) { + g_message("smf_set_ppqn failed."); + return -3; + } + + g_message("Pulses Per Quarter Note changed to %d.", smf->ppqn); + } + + return 0; +} + +static int +cmd_format(char *new_format) +{ + int tmp; + char *end; + + if (new_format == NULL) { + g_message("Format is %d.", smf->format); + } else { + tmp = strtol(new_format, &end, 10); + if (end - new_format != strlen(new_format)) { + g_critical("Invalid format value, garbage characters after the number."); + return -1; + } + + if (tmp < 0 || tmp > 2) { + g_critical("Invalid format value, valid values are in range 0 - 2, inclusive."); + return -2; + } + + if (smf_set_format(smf, tmp)) { + g_critical("smf_set_format failed."); + return -3; + } + + g_message("Forma changed to %d.", smf->format); + } + + return 0; +} + +static int +cmd_tracks(char *notused) +{ + if (smf->number_of_tracks > 0) + g_message("There are %d tracks, numbered from 1 to %d.", smf->number_of_tracks, smf->number_of_tracks); + else + g_message("There are no tracks."); + + return 0; +} + +static int +parse_track_number(const char *arg) +{ + int num; + char *end; + + if (arg == NULL) { + if (selected_track == NULL) { + g_message("No track currently selected and no track number given."); + return -1; + } else { + return selected_track->track_number; + } + } + + num = strtol(arg, &end, 10); + if (end - arg != strlen(arg)) { + g_critical("Invalid track number, garbage characters after the number."); + return -1; + } + + if (num < 1 || num > smf->number_of_tracks) { + if (smf->number_of_tracks > 0) { + g_critical("Invalid track number specified; valid choices are 1 - %d.", smf->number_of_tracks); + } else { + g_critical("There are no tracks."); + } + + return -1; + } + + return num; +} + +static int +cmd_track(char *arg) +{ + int num; + + if (arg == NULL) { + if (selected_track == NULL) + g_message("No track currently selected."); + else + g_message("Currently selected is track number %d, containing %d events.", + selected_track->track_number, selected_track->number_of_events); + } else { + if (smf->number_of_tracks == 0) { + g_message("There are no tracks."); + return -1; + } + + num = parse_track_number(arg); + if (num < 0) + return -1; + + selected_track = smf_get_track_by_number(smf, num); + if (selected_track == NULL) { + g_critical("smf_get_track_by_number() failed, track not selected."); + return -3; + } + + selected_event = NULL; + + g_message("Track number %d selected; it contains %d events.", + selected_track->track_number, selected_track->number_of_events); + } + + return 0; +} + +static int +cmd_trackadd(char *notused) +{ + selected_track = smf_track_new(); + if (selected_track == NULL) { + g_critical("smf_track_new() failed, track not created."); + return -1; + } + + smf_add_track(smf, selected_track); + + selected_event = NULL; + + g_message("Created new track; track number %d selected.", selected_track->track_number); + + return 0; +} + +static int +cmd_trackrm(char *arg) +{ + int num = parse_track_number(arg); + + if (num < 0) + return -1; + + if (selected_track != NULL && num == selected_track->track_number) { + selected_track = NULL; + selected_event = NULL; + } + + smf_track_delete(smf_get_track_by_number(smf, num)); + + g_message("Track #%d removed.", num); + + return 0; +} + +#define BUFFER_SIZE 1024 + +static int +show_event(smf_event_t *event) +{ + int off = 0, i; + char *decoded, *type; + + if (smf_event_is_metadata(event)) + type = "Metadata"; + else + type = "Event"; + + decoded = smf_event_decode(event); + + if (decoded == NULL) { + decoded = malloc(BUFFER_SIZE); + if (decoded == NULL) { + g_critical("show_event: malloc failed."); + return -1; + } + + off += snprintf(decoded + off, BUFFER_SIZE - off, "Unknown event:"); + + for (i = 0; i < event->midi_buffer_length && i < 5; i++) + off += snprintf(decoded + off, BUFFER_SIZE - off, " 0x%x", event->midi_buffer[i]); + } + + g_message("%d: %s: %s, %f seconds, %d pulses, %d delta pulses", event->event_number, type, decoded, + event->time_seconds, event->time_pulses, event->delta_time_pulses); + + free(decoded); + + return 0; +} + +static int +cmd_events(char *notused) +{ + smf_event_t *event; + + if (selected_track == NULL) { + g_critical("No track selected - please use 'track [number]' command first."); + return -1; + } + + g_message("List of events in track %d follows:", selected_track->track_number); + + smf_rewind(smf); + + while ((event = smf_track_get_next_event(selected_track)) != NULL) { + show_event(event); + } + + smf_rewind(smf); + + return 0; +} + +static int +parse_event_number(const char *arg) +{ + int num; + char *end; + + if (selected_track == NULL) { + g_critical("You need to select track first (using 'track ')."); + return -1; + } + + if (arg == NULL) { + if (selected_event == NULL) { + g_message("No event currently selected and no event number given."); + return -1; + } else { + return selected_event->event_number; + } + } + + num = strtol(arg, &end, 10); + if (end - arg != strlen(arg)) { + g_critical("Invalid event number, garbage characters after the number."); + return -1; + } + + if (num < 1 || num > selected_track->number_of_events) { + if (selected_track->number_of_events > 0) { + g_critical("Invalid event number specified; valid choices are 1 - %d.", selected_track->number_of_events); + } else { + g_critical("There are no events in currently selected track."); + } + + return -1; + } + + return num; +} + +static int +cmd_event(char *arg) +{ + int num; + + if (arg == NULL) { + if (selected_event == NULL) { + g_message("No event currently selected."); + } else { + g_message("Currently selected is event %d, track %d.", selected_event->event_number, selected_track->track_number); + show_event(selected_event); + } + } else { + num = parse_event_number(arg); + if (num < 0) + return -1; + + selected_event = smf_track_get_event_by_number(selected_track, num); + if (selected_event == NULL) { + g_critical("smf_get_event_by_number() failed, event not selected."); + return -2; + } + + g_message("Event number %d selected.", selected_event->event_number); + show_event(selected_event); + } + + return 0; +} + +static int +decode_hex(char *str, unsigned char **buffer, int *length) +{ + int i, value, midi_buffer_length; + char buf[3]; + unsigned char *midi_buffer = NULL; + char *end = NULL; + + if ((strlen(str) % 2) != 0) { + g_critical("Hex value should have even number of characters, you know."); + goto error; + } + + midi_buffer_length = strlen(str) / 2; + midi_buffer = malloc(midi_buffer_length); + if (midi_buffer == NULL) { + g_critical("malloc() failed."); + goto error; + } + + for (i = 0; i < midi_buffer_length; i++) { + buf[0] = str[i * 2]; + buf[1] = str[i * 2 + 1]; + buf[2] = '\0'; + value = strtoll(buf, &end, 16); + + if (end - buf != 2) { + g_critical("Garbage characters detected after hex."); + goto error; + } + + midi_buffer[i] = value; + } + + *buffer = midi_buffer; + *length = midi_buffer_length; + + return 0; + +error: + if (midi_buffer != NULL) + free(midi_buffer); + + return -1; +} + +static void +eventadd_usage(void) +{ + g_message("Usage: eventadd time-in-seconds midi-in-hex. For example, 'eventadd 1 903C7F'"); + g_message("will add Note On event, one second from the start of song, channel 1, note C4, velocity 127."); +} + +static int +cmd_eventadd(char *str) +{ + int midi_buffer_length; + double seconds; + unsigned char *midi_buffer; + char *time, *endtime; + + if (selected_track == NULL) { + g_critical("Please select a track first."); + return -1; + } + + if (str == NULL) { + eventadd_usage(); + return -2; + } + + /* Extract the time. */ + time = strsep(&str, " "); + seconds = strtod(time, &endtime); + if (endtime - time != strlen(time)) { + g_critical("Time is supposed to be a number, without trailing characters."); + return -3; + } + + /* Called with one parameter? */ + if (str == NULL) { + eventadd_usage(); + return -4; + } + + if (decode_hex(str, &midi_buffer, &midi_buffer_length)) { + eventadd_usage(); + return -5; + } + + selected_event = smf_event_new(); + if (selected_event == NULL) { + g_critical("smf_event_new() failed, event not created."); + return -6; + } + + selected_event->midi_buffer = midi_buffer; + selected_event->midi_buffer_length = midi_buffer_length; + + if (smf_event_is_valid(selected_event) == 0) { + g_critical("Event is invalid from the MIDI specification point of view, not created."); + smf_event_delete(selected_event); + selected_event = NULL; + return -7; + } + + smf_track_add_event_seconds(selected_track, selected_event, seconds); + + g_message("Event created."); + + return 0; +} + +static int +cmd_eventaddeot(char *time) +{ + double seconds; + char *end; + + if (selected_track == NULL) { + g_critical("Please select a track first."); + return -1; + } + + if (time == NULL) { + g_critical("Please specify the time, in seconds."); + return -2; + } + + seconds = strtod(time, &end); + if (end - time != strlen(time)) { + g_critical("Time is supposed to be a number, without trailing characters."); + return -3; + } + + if (smf_track_add_eot_seconds(selected_track, seconds)) { + g_critical("smf_track_add_eot() failed."); + return -4; + } + + g_message("Event created."); + + return 0; +} + +static int +cmd_eventrm(char *number) +{ + int num = parse_event_number(number); + + if (num < 0) + return -1; + + if (selected_event != NULL && num == selected_event->event_number) + selected_event = NULL; + + smf_event_delete(smf_track_get_event_by_number(selected_track, num)); + + g_message("Event #%d removed.", num); + + return 0; +} + +static int +cmd_tempo(char *notused) +{ + int i; + smf_tempo_t *tempo; + + for (i = 0;; i++) { + tempo = smf_get_tempo_by_number(smf, i); + if (tempo == NULL) + break; + + g_message("Tempo #%d: Starts at %d pulses, %f seconds, setting %d microseconds per quarter note, %.2f BPM.", + i, tempo->time_pulses, tempo->time_seconds, tempo->microseconds_per_quarter_note, + 60000000.0 / (double)tempo->microseconds_per_quarter_note); + g_message("Time signature: %d/%d, %d clocks per click, %d 32nd notes per quarter note.", + tempo->numerator, tempo->denominator, tempo->clocks_per_click, tempo->notes_per_note); + } + + return 0; +} + +static int +cmd_length(char *notused) +{ + g_message("Length: %d pulses, %f seconds.", smf_get_length_pulses(smf), smf_get_length_seconds(smf)); + + return 0; +} + +static int +cmd_version(char *notused) +{ + g_message("libsmf version %s.", smf_get_version()); + + return 0; +} + +static int +cmd_exit(char *notused) +{ + g_debug("Good bye."); + exit(0); +} + +static int cmd_help(char *notused); + +struct command_struct { + char *name; + int (*function)(char *command); + char *help; +} commands[] = {{"help", cmd_help, "show this help."}, + {"load", cmd_load, "load named file."}, + {"save", cmd_save, "save to named file."}, + {"ppqn", cmd_ppqn, "show ppqn (aka division), or set ppqn if used with parameter."}, + {"format", cmd_format, "show format, or set format if used with parameter."}, + {"tracks", cmd_tracks, "show number of tracks."}, + {"track", cmd_track, "show number of currently selected track, or select a track."}, + {"trackadd", cmd_trackadd, "add a track and select it."}, + {"trackrm", cmd_trackrm, "remove currently selected track."}, + {"events", cmd_events, "show events in the currently selected track."}, + {"event", cmd_event, "show number of currently selected event, or select an event."}, + {"eventadd", cmd_eventadd, "add an event and select it."}, + {"add", cmd_eventadd, NULL}, + {"eventaddeot", cmd_eventaddeot, "add an End Of Track event."}, + {"eot", cmd_eventaddeot, NULL}, + {"eventrm", cmd_eventrm, "remove currently selected event."}, + {"rm", cmd_eventrm, NULL}, + {"tempo", cmd_tempo, "show tempo map."}, + {"length", cmd_length, "show length of the song."}, + {"version", cmd_version, "show libsmf version."}, + {"exit", cmd_exit, "exit to shell."}, + {"quit", cmd_exit, NULL}, + {"bye", cmd_exit, NULL}, + {NULL, NULL, NULL}}; + +static int +cmd_help(char *notused) +{ + struct command_struct *tmp; + + g_message("Available commands:"); + + for (tmp = commands; tmp->name != NULL; tmp++) { + /* Skip commands with no help string. */ + if (tmp->help == NULL) + continue; + g_message("%s: %s", tmp->name, tmp->help); + } + + return 0; +} + +/** + * Removes (in place) all whitespace characters before the first + * non-whitespace and all trailing whitespace characters. Replaces + * more than one consecutive whitespace characters with one. + */ +static void +strip_unneeded_whitespace(char *str, int len) +{ + char *src, *dest; + int skip_white = 1; + + for (src = str, dest = str; src < dest + len; src++) { + if (*src == '\n' || *src == '\0') { + *dest = '\0'; + break; + } + + if (isspace(*src)) { + if (skip_white) + continue; + + skip_white = 1; + } else { + skip_white = 0; + } + + *dest = *src; + dest++; + } + + /* Remove trailing whitespace. */ + len = strlen(dest); + if (isspace(dest[len - 1])) + dest[len - 1] = '\0'; +} + +static char * +read_command(void) +{ + char *buf; + int len; + +#ifdef HAVE_LIBREADLINE + buf = readline("smfsh> "); +#else + buf = malloc(1024); + if (buf == NULL) { + g_critical("Malloc failed."); + return NULL; + } + + fprintf(stdout, "smfsh> "); + fflush(stdout); + + buf = fgets(buf, 1024, stdin); +#endif + + if (buf == NULL) { + fprintf(stdout, "exit\n"); + return "exit"; + } + + strip_unneeded_whitespace(buf, 1024); + + len = strlen(buf); + + if (len == 0) + return read_command(); + +#ifdef HAVE_LIBREADLINE + add_history(buf); +#endif + + return buf; +} + +static int +execute_command(char *line) +{ + char *command, *args; + struct command_struct *tmp; + + args = line; + command = strsep(&args, " "); + + for (tmp = commands; tmp->name != NULL; tmp++) { + if (strcmp(tmp->name, command) == 0) + return (tmp->function)(args); + } + + g_warning("No such command: '%s'. Type 'help' to see available commands.", command); + + return -1; +} + +static void +read_and_execute_command(void) +{ + int ret; + char *command; + + command = read_command(); + + ret = execute_command(command); + if (ret) { + g_warning("Command finished with error."); + } + + free(command); +} + +#ifdef HAVE_LIBREADLINE + +static char * +smfsh_command_generator(const char *text, int state) +{ + static struct command_struct *command = commands; + char *tmp; + + if (state == 0) + command = commands; + + while (command->name != NULL) { + tmp = command->name; + command++; + + if (strncmp(tmp, text, strlen(text)) == 0) + return strdup(tmp); + } + + return NULL; +} + +static char ** +smfsh_completion(const char *text, int start, int end) +{ + int i; + + /* Return NULL if "text" is not the first word in the input line. */ + if (start != 0) { + for (i = 0; i < start; i++) { + if (!isspace(rl_line_buffer[i])) + return NULL; + } + } + + return rl_completion_matches(text, smfsh_command_generator); +} + +#endif + +int main(int argc, char *argv[]) +{ + if (argc > 2) { + usage(); + } + + g_log_set_default_handler(log_handler, NULL); + + smf = smf_new(); + if (smf == NULL) { + g_critical("Cannot initialize smf_t."); + return -1; + } + + if (argc == 2) { + last_file_name = argv[1]; + cmd_load(last_file_name); + } else { + cmd_trackadd(NULL); + } + +#ifdef HAVE_LIBREADLINE + rl_readline_name = "smfsh"; + rl_attempted_completion_function = smfsh_completion; +#endif + + for (;;) + read_and_execute_command(); + + return 0; +} + diff --git a/man/Makefile.am b/man/Makefile.am new file mode 100644 index 0000000..cf95f87 --- /dev/null +++ b/man/Makefile.am @@ -0,0 +1,4 @@ +man_MANS = jack-smf-player.1 jack-smf-recorder.1 + +EXTRA_DIST = $(man_MANS) + diff --git a/man/jack-smf-player.1 b/man/jack-smf-player.1 new file mode 100644 index 0000000..02e700d --- /dev/null +++ b/man/jack-smf-player.1 @@ -0,0 +1,55 @@ +.\" This manpage has been automatically generated by docbook2man +.\" from a DocBook document. This tool can be found at: +.\" +.\" Please send any bug reports, improvements, comments, patches, +.\" etc. to Steve Cheng . +.TH "JACK-SMF-PLAYER" "1" "03 May 2008" "jack-smf-player 1.0" "" + +.SH NAME +jack-smf-player \- Standard MIDI File player for JACK MIDI +.SH SYNOPSIS + +\fBjack-smf-player\fR [ \fB-a \fIinput port\fB\fR ] [ \fB-d\fR ] [ \fB-n\fR ] [ \fB-q\fR ] [ \fB-s\fR ] [ \fB-t\fR ] [ \fB-V\fR ] [ \fB-r \fIrate\fB\fR ] [ \fBfile name\fR ] + +.SH "OPTIONS" +.TP +\fB-a \fIinput port\fB\fR +Automatically connect to the named input port. Note that this may cause problems with LASH. +.TP +\fB-d\fR +When throttling, drop messages instead of delaying them. +.TP +\fB-n\fR +Don't start JACK transport at startup. +.TP +\fB-q\fR +Be quiet, don't print messages about SMF metaevents etc. +.TP +\fB-s\fR +By default, \fBjack-smf-player\fR creates one output port, "midi_out", +through which MIDI messages from all tracks are sent with their original MIDI channel +number unchanged, and then one port per track, through which only MIDI messages from +that particular track are sent with their MIDI channel reset to 0. That may cause +problems with large number of tracks, as the number of JACK ports is a limited resource. +Passing the "-s" option causes \fBjack-smf-player\fR to not create +per-channel output ports. This option does not change behaviour of the "midi_out" port. +.TP +\fB-t\fR +Do not use JACK transport. That means that playback will not start when the transport +starts, will not seek when transport is relocated etc. Also, this flag will make +\fBjack-smf-player\fR exit after the end of the song. +.TP +\fB-V\fR +Print version number to standard output and exit. +.TP +\fB-r \fIrate\fB\fR +Set output rate limit to \fIrate\fR, in Kbaud. Limit +defined by the MIDI specification is 31.25. By default this parameter is zero, that +is, rate limiting is disabled. +.SH "DESCRIPTION" +.PP +\fBjack-smf-player\fR is a tool to play SMF (Standard MIDI Format) files +through JACK MIDI. At startup, it will load file which name is passed as a parameter, +and, unless the "-n" option is given, rewind JACK transport to position zero and start +playing. You can pause, play and seek using JACK transport, for example by using buttons +under the display in qjackctl. diff --git a/man/jack-smf-recorder.1 b/man/jack-smf-recorder.1 new file mode 100644 index 0000000..09796f7 --- /dev/null +++ b/man/jack-smf-recorder.1 @@ -0,0 +1,25 @@ +.\" This manpage has been automatically generated by docbook2man +.\" from a DocBook document. This tool can be found at: +.\" +.\" Please send any bug reports, improvements, comments, patches, +.\" etc. to Steve Cheng . +.TH "JACK-SMF-RECORDER" "1" "22 April 2008" "jack-smf-recorder 1.0" "" + +.SH NAME +jack-smf-recorder \- Standard MIDI File recorder for JACK MIDI +.SH SYNOPSIS + +\fBjack-smf-recorder\fR [ \fB-a \fIoutput port\fB\fR ] [ \fB-V\fR ] [ \fBfile name\fR ] + +.SH "OPTIONS" +.TP +\fB-a \fIoutput port\fB\fR +Automatically connect to the named output port. Note that this may cause problems with LASH. +.TP +\fB-V\fR +Print version number to standard output and exit. +.SH "DESCRIPTION" +.PP +\fBjack-smf-recorder\fR is a tool to record SMF (Standard MIDI Format) files +using JACK MIDI. Recording starts at the first received MIDI event. File will be written +after pressing ^C. \fBjack-smf-recorder\fR does not use JACK transport. diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..532f06c --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,10 @@ +bin_PROGRAMS = jack-smf-player jack-smf-recorder +jack_smf_player_SOURCES = jack-smf-player.c +jack_smf_recorder_SOURCES = jack-smf-recorder.c +jack_smf_player_LDADD = $(GLIB_LIBS) $(GTHREAD_LIBS) $(JACK_LIBS) $(LASH_LIBS) ../libsmf/libsmf.a +jack_smf_recorder_LDADD = $(jack_smf_player_LDADD) +jack_smf_player_CFLAGS = $(GLIB_CFLAGS) $(GTHREAD_CFLAGS) $(JACK_CFLAGS) $(LASH_CFLAGS) \ + -I$(top_srcdir)/libsmf -DG_LOG_DOMAIN=\"jack-smf-player\" +jack_smf_recorder_CFLAGS = $(GLIB_CFLAGS) $(GTHREAD_CFLAGS) $(JACK_CFLAGS) $(LASH_CFLAGS) \ + -I$(top_srcdir)/libsmf -DG_LOG_DOMAIN=\"jack-smf-recorder\" + diff --git a/src/jack-smf-player.c b/src/jack-smf-player.c new file mode 100644 index 0000000..3745894 --- /dev/null +++ b/src/jack-smf-player.c @@ -0,0 +1,771 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * ALTHOUGH THIS SOFTWARE IS MADE OF WIN AND SCIENCE, IT IS PROVIDED BY THE + * AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +/* + * This is jack-smf-player, Standard MIDI File player for JACK MIDI. + * + * For questions and comments, contact Edward Tomasz Napierala . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "smf.h" + +#ifdef WITH_LASH +#include +#endif + +#define PROGRAM_NAME "jack-smf-player" +#define PROGRAM_VERSION PACKAGE_VERSION + +#define MIDI_CONTROLLER 0xB0 +#define MIDI_ALL_SOUND_OFF 120 + +#define MAX_NUMBER_OF_TRACKS 128 + +jack_port_t *output_ports[MAX_NUMBER_OF_TRACKS]; +int drop_messages = 0; +jack_client_t *jack_client = NULL; +double rate_limit = 0; +int just_one_output = 0; +int start_stopped = 0; +int use_transport = 1; +int be_quiet = 0; +volatile int playback_started = -1, song_position = 0, ctrl_c_pressed = 0; +smf_t *smf = NULL; + +#ifdef WITH_LASH +lash_client_t *lash_client; +#endif + +/* Will emit a warning if time between jack callbacks is longer than this. */ +#define MAX_TIME_BETWEEN_CALLBACKS 0.1 + +/* Will emit a warning if execution of jack callback takes longer than this. */ +#define MAX_PROCESSING_TIME 0.01 + +double +get_time(void) +{ + double seconds; + int ret; + struct timeval tv; + + ret = gettimeofday(&tv, NULL); + + if (ret) { + perror("gettimeofday"); + exit(EX_OSERR); + } + + seconds = tv.tv_sec + tv.tv_usec / 1000000.0; + + return seconds; +} + +double +get_delta_time(void) +{ + static double previously = -1.0; + double now; + double delta; + + now = get_time(); + + if (previously == -1.0) { + previously = now; + + return 0; + } + + delta = now - previously; + previously = now; + + assert(delta >= 0.0); + + return delta; +} + +static gboolean +warning_async(gpointer s) +{ + const char *str = (const char *)s; + + g_warning(str); + + return FALSE; +} + +static void +warn_from_jack_thread_context(const char *str) +{ + g_idle_add(warning_async, (gpointer)str); +} + +static double +nframes_to_ms(jack_nframes_t nframes) +{ + jack_nframes_t sr; + + sr = jack_get_sample_rate(jack_client); + + assert(sr > 0); + + return (nframes * 1000.0) / (double)sr; +} + +static double +nframes_to_seconds(jack_nframes_t nframes) +{ + return nframes_to_ms(nframes) / 1000.0; +} + +static jack_nframes_t +ms_to_nframes(double ms) +{ + jack_nframes_t sr; + + sr = jack_get_sample_rate(jack_client); + + assert(sr > 0); + + return ((double)sr * ms) / 1000.0; +} + +static jack_nframes_t +seconds_to_nframes(double seconds) +{ + return ms_to_nframes(seconds * 1000.0); +} + +static void +send_all_sound_off(void *port_buffers[MAX_NUMBER_OF_TRACKS], jack_nframes_t nframes) +{ + int i, channel; + unsigned char *buffer; + + for (i = 0; i <= smf->number_of_tracks; i++) { + for (channel = 0; channel < 16; channel++) { +#ifdef JACK_MIDI_NEEDS_NFRAMES + buffer = jack_midi_event_reserve(port_buffers[i], 0, 3, nframes); +#else + buffer = jack_midi_event_reserve(port_buffers[i], 0, 3); +#endif + if (buffer == NULL) { + warn_from_jack_thread_context("jack_midi_event_reserve failed, cannot send All Sound Off."); + break; + } + + buffer[0] = MIDI_CONTROLLER | channel; + buffer[1] = MIDI_ALL_SOUND_OFF; + buffer[2] = 0; + } + + if (just_one_output) + break; + } +} + +static void +process_midi_output(jack_nframes_t nframes) +{ + int i, t, bytes_remaining, track_number; + unsigned char *buffer, tmp_status; + void *port_buffers[MAX_NUMBER_OF_TRACKS]; + jack_nframes_t last_frame_time; + jack_transport_state_t transport_state; + static jack_transport_state_t previous_transport_state = JackTransportStopped; + + for (i = 0; i <= smf->number_of_tracks; i++) { + port_buffers[i] = jack_port_get_buffer(output_ports[i], nframes); + + if (port_buffers[i] == NULL) { + warn_from_jack_thread_context("jack_port_get_buffer failed, cannot send anything."); + return; + } + +#ifdef JACK_MIDI_NEEDS_NFRAMES + jack_midi_clear_buffer(port_buffers[i], nframes); +#else + jack_midi_clear_buffer(port_buffers[i]); +#endif + + if (just_one_output) + break; + } + + if (ctrl_c_pressed) { + send_all_sound_off(port_buffers, nframes); + + /* The idea here is to exit at the second time process_midi_output gets called. + Otherwise, All Sound Off won't be delivered. */ + ctrl_c_pressed++; + if (ctrl_c_pressed >= 3) + exit(0); + + return; + } + + if (use_transport) { + transport_state = jack_transport_query(jack_client, NULL); + if (transport_state == JackTransportStopped) { + if (previous_transport_state == JackTransportRolling) + send_all_sound_off(port_buffers, nframes); + + previous_transport_state = transport_state; + + return; + } + + previous_transport_state = transport_state; + } + + last_frame_time = jack_last_frame_time(jack_client); + + /* End of song already? */ + if (playback_started < 0) + return; + + /* We may push at most one byte per 0.32ms to stay below 31.25 Kbaud limit. */ + bytes_remaining = nframes_to_ms(nframes) * rate_limit; + + for (;;) { + smf_event_t *event = smf_peek_next_event(smf); + + if (event == NULL) { + if (!be_quiet) + g_debug("End of song."); + playback_started = -1; + + if (!use_transport) + ctrl_c_pressed = 1; + + break; + } + + /* Skip over metadata events. */ + if (smf_event_is_metadata(event)) { + char *decoded = smf_event_decode(event); + if (decoded && !be_quiet) + g_debug("Metadata: %s", decoded); + + smf_get_next_event(smf); + continue; + } + + bytes_remaining -= event->midi_buffer_length; + + if (rate_limit > 0.0 && bytes_remaining <= 0) { + warn_from_jack_thread_context("Rate limiting in effect."); + break; + } + + t = seconds_to_nframes(event->time_seconds) + playback_started - song_position + nframes - last_frame_time; + + /* If computed time is too much into the future, we'll need + to send it later. */ + if (t >= (int)nframes) + break; + + /* If computed time is < 0, we missed a cycle because of xrun. */ + if (t < 0) + t = 0; + + assert(event->track->track_number >= 0 && event->track->track_number <= MAX_NUMBER_OF_TRACKS); + + /* We will send this event; remove it from the queue. */ + smf_get_next_event(smf); + + /* First, send it via midi_out. */ + track_number = 0; + +#ifdef JACK_MIDI_NEEDS_NFRAMES + buffer = jack_midi_event_reserve(port_buffers[track_number], t, event->midi_buffer_length, nframes); +#else + buffer = jack_midi_event_reserve(port_buffers[track_number], t, event->midi_buffer_length); +#endif + + if (buffer == NULL) { + warn_from_jack_thread_context("jack_midi_event_reserve failed, NOTE LOST."); + break; + } + + memcpy(buffer, event->midi_buffer, event->midi_buffer_length); + + /* Ignore per-track outputs? */ + if (just_one_output) + continue; + + /* Send it via proper output port. */ + track_number = event->track->track_number; + +#ifdef JACK_MIDI_NEEDS_NFRAMES + buffer = jack_midi_event_reserve(port_buffers[track_number], t, event->midi_buffer_length, nframes); +#else + buffer = jack_midi_event_reserve(port_buffers[track_number], t, event->midi_buffer_length); +#endif + + if (buffer == NULL) { + warn_from_jack_thread_context("jack_midi_event_reserve failed, NOTE LOST."); + break; + } + + /* Before sending, reset channel to 0. XXX: Not very pretty. */ + assert(event->midi_buffer_length >= 1); + + tmp_status = event->midi_buffer[0]; + + if (event->midi_buffer[0] >= 0x80 && event->midi_buffer[0] <= 0xEF) + event->midi_buffer[0] &= 0xF0; + + memcpy(buffer, event->midi_buffer, event->midi_buffer_length); + + event->midi_buffer[0] = tmp_status; + } +} + +static int +process_callback(jack_nframes_t nframes, void *notused) +{ +#ifdef MEASURE_TIME + if (get_delta_time() > MAX_TIME_BETWEEN_CALLBACKS) { + warn_from_jack_thread_context("Had to wait too long for JACK callback; scheduling problem?"); + } +#endif + + /* Check for impossible condition that actually happened to me, caused by some problem between jackd and OSS4. */ + if (nframes <= 0) { + warn_from_jack_thread_context("Process callback called with nframes = 0; bug in JACK?"); + return 0; + } + + process_midi_output(nframes); + +#ifdef MEASURE_TIME + if (get_delta_time() > MAX_PROCESSING_TIME) { + warn_from_jack_thread_context("Processing took too long; scheduling problem?"); + } +#endif + + return 0; +} + +static int +sync_callback(jack_transport_state_t state, jack_position_t *position, void *notused) +{ + assert(jack_client); + + /* XXX: We should probably adapt to external tempo changes. */ + + if (state == JackTransportStarting) { + song_position = position->frame; + smf_seek_to_seconds(smf, nframes_to_seconds(position->frame)); + + if (!be_quiet) + g_debug("Seeking to %f seconds.", nframes_to_seconds(position->frame)); + + playback_started = jack_frame_time(jack_client); + + } else if (state == JackTransportStopped) { + playback_started = -1; + } + + return TRUE; +} + +void timebase_callback(jack_transport_state_t state, jack_nframes_t nframes, jack_position_t *pos, int new_pos, void *notused) +{ + double min; /* Minutes since frame 0. */ + long abs_tick; /* Ticks since frame 0. */ + long abs_beat; /* Beats since frame 0. */ + smf_tempo_t *tempo; + static smf_tempo_t *previous_tempo = NULL; + + smf_event_t *event = smf_peek_next_event(smf); + if (event == NULL) + return; + + tempo = smf_get_tempo_by_pulses(smf, event->time_pulses); + + assert(tempo); + + if (new_pos || previous_tempo != tempo) { + pos->valid = JackPositionBBT; + pos->beats_per_bar = tempo->numerator; + pos->beat_type = 1.0 / (double)tempo->denominator; + pos->ticks_per_beat = event->track->smf->ppqn; /* XXX: Is this right? */ + pos->beats_per_minute = 60000000.0 / (double)tempo->microseconds_per_quarter_note; + + min = pos->frame / ((double) pos->frame_rate * 60.0); + abs_tick = min * pos->beats_per_minute * pos->ticks_per_beat; + abs_beat = abs_tick / pos->ticks_per_beat; + + pos->bar = abs_beat / pos->beats_per_bar; + pos->beat = abs_beat - (pos->bar * pos->beats_per_bar) + 1; + pos->tick = abs_tick - (abs_beat * pos->ticks_per_beat); + pos->bar_start_tick = pos->bar * pos->beats_per_bar * pos->ticks_per_beat; + pos->bar++; /* adjust start to bar 1 */ + + previous_tempo = tempo; + + } else { + /* Compute BBT info based on previous period. */ + pos->tick += nframes * pos->ticks_per_beat * pos->beats_per_minute / (pos->frame_rate * 60); + + while (pos->tick >= pos->ticks_per_beat) { + pos->tick -= pos->ticks_per_beat; + if (++pos->beat > pos->beats_per_bar) { + pos->beat = 1; + ++pos->bar; + pos->bar_start_tick += pos->beats_per_bar * pos->ticks_per_beat; + } + } + } +} + +/* Connects to the specified input port, disconnecting already connected ports. */ +int +connect_to_input_port(const char *port) +{ + int ret; + + ret = jack_port_disconnect(jack_client, output_ports[0]); + + if (ret) { + g_warning("Cannot disconnect MIDI port."); + + return -3; + } + + ret = jack_connect(jack_client, jack_port_name(output_ports[0]), port); + + if (ret) { + g_warning("Cannot connect to %s.", port); + + return -4; + } + + g_warning("Connected to %s.", port); + + return 0; +} + +static void +init_jack(void) +{ + int i, err; + +#ifdef WITH_LASH + lash_event_t *event; +#endif + + jack_client = jack_client_open(PROGRAM_NAME, JackNullOption, NULL); + + if (jack_client == NULL) { + g_critical("Could not connect to the JACK server; run jackd first?"); + exit(EX_UNAVAILABLE); + } + +#ifdef WITH_LASH + event = lash_event_new_with_type(LASH_Client_Name); + assert (event); /* Documentation does not say anything about return value. */ + lash_event_set_string(event, jack_get_client_name(jack_client)); + lash_send_event(lash_client, event); + + lash_jack_client_name(lash_client, jack_get_client_name(jack_client)); +#endif + + err = jack_set_process_callback(jack_client, process_callback, 0); + if (err) { + g_critical("Could not register JACK process callback."); + exit(EX_UNAVAILABLE); + } + + if (use_transport) { + err = jack_set_sync_callback(jack_client, sync_callback, 0); + if (err) { + g_critical("Could not register JACK sync callback."); + exit(EX_UNAVAILABLE); + } +#if 0 + err = jack_set_timebase_callback(jack_client, 1, timebase_callback, 0); + if (err) { + g_critical("Could not register JACK timebase callback."); + exit(EX_UNAVAILABLE); + } +#endif + } + + assert(smf->number_of_tracks >= 1); + + /* We are allocating number_of_tracks + 1 output ports. */ + for (i = 0; i <= smf->number_of_tracks; i++) { + char port_name[32]; + + if (i == 0) + snprintf(port_name, sizeof(port_name), "midi_out"); + else + snprintf(port_name, sizeof(port_name), "track_%d_midi_out", i); + + output_ports[i] = jack_port_register(jack_client, port_name, JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0); + + if (output_ports[i] == NULL) { + g_critical("Could not register JACK output port '%s'.", port_name); + exit(EX_UNAVAILABLE); + } + + if (just_one_output) + break; + } + + if (jack_activate(jack_client)) { + g_critical("Cannot activate JACK client."); + exit(EX_UNAVAILABLE); + } +} + +#ifdef WITH_LASH + +static gboolean +lash_callback(gpointer notused) +{ + lash_event_t *event; + + while ((event = lash_get_event(lash_client))) { + switch (lash_event_get_type(event)) { + case LASH_Restore_Data_Set: + case LASH_Save_Data_Set: + break; + + case LASH_Quit: + g_warning("Exiting due to LASH request."); + ctrl_c_pressed = 1; + break; + + default: + g_warning("Receieved unknown LASH event of type %d.", lash_event_get_type(event)); + lash_event_destroy(event); + } + } + + return TRUE; +} + +static void +init_lash(lash_args_t *args) +{ + /* XXX: Am I doing the right thing wrt protocol version? */ + lash_client = lash_init(args, PROGRAM_NAME, LASH_Config_Data_Set, LASH_PROTOCOL(2, 0)); + + if (!lash_server_connected(lash_client)) { + g_critical("Cannot initialize LASH. Continuing anyway."); + /* exit(EX_UNAVAILABLE); */ + + return; + } + + /* Schedule a function to process LASH events, ten times per second. */ + g_timeout_add(100, lash_callback, NULL); +} + +#endif /* WITH_LASH */ + +/* + * This is neccessary for exiting due to jackd being killed, when exit(0) + * in process_callback won't get called for obvious reasons. + */ +gboolean +emergency_exit_timeout(gpointer notused) +{ + if (ctrl_c_pressed == 0) + return TRUE; + + exit(0); +} + +void +ctrl_c_handler(int signum) +{ + ctrl_c_pressed = 1; +} + +static void +log_handler(const gchar *log_domain, GLogLevelFlags log_level, const gchar *message, gpointer notused) +{ + fprintf(stderr, "%s: %s\n", log_domain, message); +} + +static void +show_version(void) +{ + fprintf(stdout, "%s %s, libsmf %s\n", PROGRAM_NAME, PROGRAM_VERSION, smf_get_version()); + + exit(EX_OK); +} + +static void +usage(void) +{ + fprintf(stderr, "usage: jack-smf-player [-dnqstV] [ -a ] [-r ] file_name\n"); + + exit(EX_USAGE); +} + +int +main(int argc, char *argv[]) +{ + int ch; + char *file_name, *autoconnect_port_name = NULL; + +#ifdef WITH_LASH + lash_args_t *lash_args; +#endif + + g_thread_init(NULL); + +#ifdef WITH_LASH + lash_args = lash_extract_args(&argc, &argv); +#endif + + g_log_set_default_handler(log_handler, NULL); + + while ((ch = getopt(argc, argv, "a:dnqr:stV")) != -1) { + switch (ch) { + case 'a': + autoconnect_port_name = strdup(optarg); + break; + + case 'd': + drop_messages = 1; + break; + + case 'n': + start_stopped = 1; + break; + + case 'q': + be_quiet = 1; + break; + + case 'r': + rate_limit = strtod(optarg, NULL); + if (rate_limit <= 0.0) { + g_critical("Invalid rate limit specified.\n"); + + exit(EX_USAGE); + } + + break; + + case 's': + just_one_output = 1; + break; + + case 't': + use_transport = 0; + break; + + case 'V': + show_version(); + break; + + case '?': + default: + usage(); + } + } + + argc -= optind; + argv += optind; + + if (argv[0] == NULL) { + g_critical("No file name given."); + usage(); + } + + file_name = argv[0]; + + smf = smf_load(file_name); + + if (smf == NULL) { + g_critical("Loading SMF file failed."); + + exit(-1); + } + + if (!be_quiet) + g_message("%s.", smf_decode(smf)); + + if (smf->number_of_tracks > MAX_NUMBER_OF_TRACKS) { + g_warning("Number of tracks (%d) exceeds maximum for per-track output; implying '-s' option.", smf->number_of_tracks); + just_one_output = 1; + } + +#ifdef WITH_LASH + init_lash(lash_args); +#endif + + g_timeout_add(1000, emergency_exit_timeout, (gpointer)0); + signal(SIGINT, ctrl_c_handler); + + init_jack(); + + if (autoconnect_port_name) { + if (connect_to_input_port(autoconnect_port_name)) { + g_critical("Couldn't connect to '%s', exiting.", autoconnect_port_name); + exit(EX_UNAVAILABLE); + } + } + + if (use_transport && !start_stopped) { + jack_transport_locate(jack_client, 0); + jack_transport_start(jack_client); + } + + if (!use_transport) + playback_started = jack_frame_time(jack_client); + + g_main_loop_run(g_main_loop_new(NULL, TRUE)); + + /* Not reached. */ + + return 0; +} + diff --git a/src/jack-smf-recorder.c b/src/jack-smf-recorder.c new file mode 100644 index 0000000..b599964 --- /dev/null +++ b/src/jack-smf-recorder.c @@ -0,0 +1,498 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * ALTHOUGH THIS SOFTWARE IS MADE OF WIN AND SCIENCE, IT IS PROVIDED BY THE + * AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +/* + * This is jack-smf-recorder, Standard MIDI File recorder for JACK MIDI. + * + * For questions and comments, contact Edward Tomasz Napierala . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "smf.h" + +#ifdef WITH_LASH +#include +#endif + +#define INPUT_PORT_NAME "midi_in" +#define PROGRAM_NAME "jack-smf-recorder" +#define PROGRAM_VERSION PACKAGE_VERSION + +jack_client_t *jack_client = NULL; +jack_port_t *input_port; +volatile int ctrl_c_pressed = 0; +smf_t *smf = NULL; +smf_track_t *tracks[16]; /* We allocate one track per MIDI channel. */ + +#ifdef WITH_LASH +lash_client_t *lash_client; +#endif + +/* Will emit a warning if time between jack callbacks is longer than this. */ +#define MAX_TIME_BETWEEN_CALLBACKS 0.1 + +/* Will emit a warning if execution of jack callback takes longer than this. */ +#define MAX_PROCESSING_TIME 0.01 + +double +get_time(void) +{ + double seconds; + int ret; + struct timeval tv; + + ret = gettimeofday(&tv, NULL); + + if (ret) { + perror("gettimeofday"); + exit(EX_OSERR); + } + + seconds = tv.tv_sec + tv.tv_usec / 1000000.0; + + return seconds; +} + +double +get_delta_time(void) +{ + static double previously = -1.0; + double now; + double delta; + + now = get_time(); + + if (previously == -1.0) { + previously = now; + + return 0; + } + + delta = now - previously; + previously = now; + + assert(delta >= 0.0); + + return delta; +} + +static gboolean +warning_async(gpointer s) +{ + const char *str = (const char *)s; + + g_warning(str); + + return FALSE; +} + +static void +warn_from_jack_thread_context(const char *str) +{ + g_idle_add(warning_async, (gpointer)str); +} + +static double +nframes_to_ms(jack_nframes_t nframes) +{ + jack_nframes_t sr; + + sr = jack_get_sample_rate(jack_client); + + assert(sr > 0); + + return (nframes * 1000.0) / (double)sr; +} + +static double +nframes_to_seconds(jack_nframes_t nframes) +{ + return nframes_to_ms(nframes) / 1000.0; +} + +void +process_midi_input(jack_nframes_t nframes) +{ + int read, events, i, channel; + void *port_buffer; + jack_midi_event_t event; + int last_frame_time; + static int time_of_first_event = -1; + + last_frame_time = jack_last_frame_time(jack_client); + + port_buffer = jack_port_get_buffer(input_port, nframes); + if (port_buffer == NULL) { + warn_from_jack_thread_context("jack_port_get_buffer failed, cannot receive anything."); + return; + } + +#ifdef JACK_MIDI_NEEDS_NFRAMES + events = jack_midi_get_event_count(port_buffer, nframes); +#else + events = jack_midi_get_event_count(port_buffer); +#endif + + for (i = 0; i < events; i++) { + smf_event_t *smf_event; + +#ifdef JACK_MIDI_NEEDS_NFRAMES + read = jack_midi_event_get(&event, port_buffer, i, nframes); +#else + read = jack_midi_event_get(&event, port_buffer, i); +#endif + if (read) { + warn_from_jack_thread_context("jack_midi_event_get failed, RECEIVED NOTE LOST."); + continue; + } + + /* Ignore realtime messages. */ + if (event.buffer[0] >= 0xF8) + continue; + + /* First event received? */ + if (time_of_first_event == -1) + time_of_first_event = last_frame_time + event.time; + + smf_event = smf_event_new_from_pointer(event.buffer, event.size); + if (smf_event == NULL) { + warn_from_jack_thread_context("smf_event_from_pointer failed, RECEIVED NOTE LOST."); + continue; + } + + assert(smf_event->midi_buffer_length >= 1); + channel = smf_event->midi_buffer[0] & 0x0F; + + smf_track_add_event_seconds(tracks[channel], smf_event, + nframes_to_seconds(jack_last_frame_time(jack_client) + event.time - time_of_first_event)); + } +} + +static int +process_callback(jack_nframes_t nframes, void *notused) +{ +#ifdef MEASURE_TIME + if (get_delta_time() > MAX_TIME_BETWEEN_CALLBACKS) { + warn_from_jack_thread_context("Had to wait too long for JACK callback; scheduling problem?"); + } +#endif + + /* Check for impossible condition that actually happened to me, caused by some problem between jackd and OSS4. */ + if (nframes <= 0) { + warn_from_jack_thread_context("Process callback called with nframes = 0; bug in JACK?"); + return 0; + } + + process_midi_input(nframes); + +#ifdef MEASURE_TIME + if (get_delta_time() > MAX_PROCESSING_TIME) { + warn_from_jack_thread_context("Processing took too long; scheduling problem?"); + } +#endif + + return 0; +} + +/* Connects to the specified input port, disconnecting already connected ports. */ +int +connect_to_output_port(const char *port) +{ + int ret; + + ret = jack_port_disconnect(jack_client, input_port); + + if (ret) { + g_warning("Cannot disconnect MIDI port."); + + return -3; + } + + ret = jack_connect(jack_client, port, jack_port_name(input_port)); + + if (ret) { + g_warning("Cannot connect to %s.", port); + + return -4; + } + + g_warning("Connected to %s.", port); + + return 0; +} + +void +init_jack(void) +{ + int err; + +#ifdef WITH_LASH + lash_event_t *event; +#endif + + jack_client = jack_client_open(PROGRAM_NAME, JackNullOption, NULL); + + if (jack_client == NULL) { + g_critical("Could not connect to the JACK server; run jackd first?"); + exit(EX_UNAVAILABLE); + } + +#ifdef WITH_LASH + event = lash_event_new_with_type(LASH_Client_Name); + assert (event); /* Documentation does not say anything about return value. */ + lash_event_set_string(event, jack_get_client_name(jack_client)); + lash_send_event(lash_client, event); + + lash_jack_client_name(lash_client, jack_get_client_name(jack_client)); +#endif + + err = jack_set_process_callback(jack_client, process_callback, 0); + if (err) { + g_critical("Could not register JACK process callback."); + exit(EX_UNAVAILABLE); + } + + input_port = jack_port_register(jack_client, INPUT_PORT_NAME, JACK_DEFAULT_MIDI_TYPE, + JackPortIsInput, 0); + + if (input_port == NULL) { + g_critical("Could not register JACK input port."); + exit(EX_UNAVAILABLE); + } + + if (jack_activate(jack_client)) { + g_critical("Cannot activate JACK client."); + exit(EX_UNAVAILABLE); + } +} + +#ifdef WITH_LASH + +static gboolean +lash_callback(gpointer notused) +{ + lash_event_t *event; + + while ((event = lash_get_event(lash_client))) { + switch (lash_event_get_type(event)) { + case LASH_Restore_Data_Set: + case LASH_Save_Data_Set: + break; + + case LASH_Quit: + g_warning("Exiting due to LASH request."); + ctrl_c_pressed = 1; + break; + + default: + g_warning("Receieved unknown LASH event of type %d.", lash_event_get_type(event)); + lash_event_destroy(event); + } + } + + return TRUE; +} + +static void +init_lash(lash_args_t *args) +{ + /* XXX: Am I doing the right thing wrt protocol version? */ + lash_client = lash_init(args, PROGRAM_NAME, LASH_Config_Data_Set, LASH_PROTOCOL(2, 0)); + + if (!lash_server_connected(lash_client)) { + g_critical("Cannot initialize LASH. Continuing anyway."); + /* exit(EX_UNAVAILABLE); */ + + return; + } + + /* Schedule a function to process LASH events, ten times per second. */ + g_timeout_add(100, lash_callback, NULL); +} + +#endif /* WITH_LASH */ + +gboolean +writer_timeout(gpointer file_name_gpointer) +{ + int i; + char *file_name = (char *)file_name_gpointer; + + /* + * XXX: It should be done like this: http://wwwtcs.inf.tu-dresden.de/~tews/Gtk/x2992.html + */ + if (ctrl_c_pressed == 0) + return TRUE; + + jack_deactivate(jack_client); + + /* Get rid of empty tracks. */ + smf_rewind(smf); + + for (i = 0; i < 16; i++) { + if (tracks[i]->number_of_events == 0) { + smf_remove_track(tracks[i]); + smf_track_delete(tracks[i]); + } + } + + if (smf->number_of_tracks == 0) { + g_message("No events recorded, not saving anything."); + exit(0); + } + + if (smf_save(smf, file_name)) { + g_critical("Could not save file '%s', sorry.", file_name); + exit(-1); + } + + g_message("File '%s' saved successfully.", file_name); + + exit(0); +} + +void +ctrl_c_handler(int signum) +{ + ctrl_c_pressed = 1; +} + +static void +log_handler(const gchar *log_domain, GLogLevelFlags log_level, const gchar *message, gpointer notused) +{ + fprintf(stderr, "%s: %s\n", log_domain, message); +} + +static void +show_version(void) +{ + fprintf(stdout, "%s %s, libsmf %s\n", PROGRAM_NAME, PROGRAM_VERSION, smf_get_version()); + + exit(EX_OK); +} + +static void +usage(void) +{ + fprintf(stderr, "usage: jack-smf-recorder [-V] [ -a ] file_name\n"); + + exit(EX_USAGE); +} + +int +main(int argc, char *argv[]) +{ + int ch, i; + char *file_name, *autoconnect_port_name = NULL; + +#ifdef WITH_LASH + lash_args_t *lash_args; +#endif + + g_thread_init(NULL); + +#ifdef WITH_LASH + lash_args = lash_extract_args(&argc, &argv); +#endif + + g_log_set_default_handler(log_handler, NULL); + + while ((ch = getopt(argc, argv, "a:V")) != -1) { + switch (ch) { + case 'a': + autoconnect_port_name = strdup(optarg); + break; + + case 'V': + show_version(); + break; + + case '?': + default: + usage(); + } + } + + argc -= optind; + argv += optind; + + if (argc == 0) + usage(); + + file_name = argv[0]; + + smf = smf_new(); + + if (smf == NULL) + exit(-1); + + for (i = 0; i < 16; i++) { + tracks[i] = smf_track_new(); + if (tracks[i] == NULL) + exit(-1); + smf_add_track(smf, tracks[i]); + } + +#ifdef WITH_LASH + init_lash(lash_args); +#endif + + init_jack(); + + if (autoconnect_port_name) { + if (connect_to_output_port(autoconnect_port_name)) { + g_critical("Couldn't connect to '%s', exiting.", autoconnect_port_name); + exit(EX_UNAVAILABLE); + } + } + + g_timeout_add(100, writer_timeout, (gpointer)argv[0]); + signal(SIGINT, ctrl_c_handler); + + g_message("Recording will start at the first received note; press ^C to write the file and exit."); + + g_main_loop_run(g_main_loop_new(NULL, TRUE)); + + /* Not reached. */ + + return 0; +} + -- 2.11.4.GIT