diff options
author | Paul Davis <paul@linuxaudiosystems.com> | 2019-10-25 13:41:33 -0600 |
---|---|---|
committer | Paul Davis <paul@linuxaudiosystems.com> | 2019-11-02 16:32:18 -0600 |
commit | eaae38ba842c4acf079fbc6e405af433576ad16e (patch) | |
tree | 1e7c88f36564795f6bbe09cb2e0e3534da5c8763 /libs/evoral/libsmf | |
parent | a855119bdd94aad90f4cfec3066a367b0675a8e9 (diff) |
move evoral/src/* to evoral/
Diffstat (limited to 'libs/evoral/libsmf')
-rw-r--r-- | libs/evoral/libsmf/COPYING | 24 | ||||
-rw-r--r-- | libs/evoral/libsmf/README | 4 | ||||
-rw-r--r-- | libs/evoral/libsmf/smf.c | 1137 | ||||
-rw-r--r-- | libs/evoral/libsmf/smf.h | 418 | ||||
-rw-r--r-- | libs/evoral/libsmf/smf_decode.c | 646 | ||||
-rw-r--r-- | libs/evoral/libsmf/smf_load.c | 984 | ||||
-rw-r--r-- | libs/evoral/libsmf/smf_private.h | 81 | ||||
-rw-r--r-- | libs/evoral/libsmf/smf_save.c | 691 | ||||
-rw-r--r-- | libs/evoral/libsmf/smf_tempo.c | 454 | ||||
-rw-r--r-- | libs/evoral/libsmf/smfsh.c | 1034 |
10 files changed, 5473 insertions, 0 deletions
diff --git a/libs/evoral/libsmf/COPYING b/libs/evoral/libsmf/COPYING new file mode 100644 index 0000000000..79ec85f5bf --- /dev/null +++ b/libs/evoral/libsmf/COPYING @@ -0,0 +1,24 @@ +Copyright (c) 2007, 2008 Edward Tomasz Napierała <trasz@FreeBSD.org> +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/libs/evoral/libsmf/README b/libs/evoral/libsmf/README new file mode 100644 index 0000000000..c1a403564c --- /dev/null +++ b/libs/evoral/libsmf/README @@ -0,0 +1,4 @@ +This is a stripped down version of libsmf 1.2 by Edward Tomasz Napiera +for internal use by Evoral. See COPYING for licensing information. + +The complete version can be found at <http://libsmf.sf.net>. diff --git a/libs/evoral/libsmf/smf.c b/libs/evoral/libsmf/smf.c new file mode 100644 index 0000000000..1fcdb8ec38 --- /dev/null +++ b/libs/evoral/libsmf/smf.c @@ -0,0 +1,1137 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała <trasz@FreeBSD.org> + * 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. + * + */ + +/** + * \file + * + * Various functions. + * + */ + +/* Reference: http://www.borg.com/~jglatt/tech/midifile.htm */ + +#include <stdlib.h> +#include <string.h> +#include <assert.h> +#include <math.h> +#include <errno.h> +#ifdef PLATFORM_WINDOWS +#include <winsock2.h> +#else +#include <arpa/inet.h> +#endif +#include "smf.h" +#include "smf_private.h" + +/** + * Allocates new smf_t structure. + * \return pointer to smf_t or NULL. + */ +smf_t * +smf_new(void) +{ + int cantfail; + + smf_t *smf = (smf_t*)malloc(sizeof(smf_t)); + if (smf == NULL) { + g_warning("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); + + cantfail = smf_set_ppqn(smf, 120); + assert(!cantfail); + + cantfail = smf_set_format(smf, 0); + assert(!cantfail); + + 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((smf_track_t*)g_ptr_array_index(smf->tracks_array, smf->tracks_array->len - 1)); + + smf_fini_tempo(smf); + + 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 = (smf_track_t*)malloc(sizeof(smf_track_t)); + if (track == NULL) { + g_warning("Cannot allocate smf_track_t structure: %s", strerror(errno)); + return (NULL); + } + + memset(track, 0, sizeof(smf_track_t)); + track->next_event_number = 0; + + 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 */ + unsigned int i; + for (i = 0; i < track->events_array->len; ++i) { + smf_event_t* ev = (smf_event_t*)g_ptr_array_index(track->events_array, i); + free (ev->midi_buffer); + free (ev); + } + + g_ptr_array_remove_range(track->events_array, 0, track->events_array->len); + track->number_of_events = 0; + + if (track->smf) + smf_track_remove_from_smf(track); + + assert(track->events_array->len == 0); + g_ptr_array_free(track->events_array, TRUE); + + if (track->name) { + free (track->name); + } + if (track->instrument) { + free (track->instrument); + } + + 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) +{ +#ifndef NDEBUG + int cantfail; +#endif + + 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) { +#ifndef NDEBUG + cantfail = smf_set_format(smf, 1); + assert(!cantfail); +#else + smf_set_format(smf, 1); +#endif + + } +} + +/** + * Detaches track from the smf. + */ +void +smf_track_remove_from_smf(smf_track_t *track) +{ + int i; + size_t j; + smf_track_t *tmp; + smf_event_t *ev; + + 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; + + /* + * Events have track numbers too. I guess this wasn't a wise + * decision. ;-/ + */ + for (j = 1; j <= tmp->number_of_events; j++) { + ev = smf_track_get_event_by_number(tmp, j); + ev->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 = (smf_event_t*)malloc(sizeof(smf_event_t)); + if (event == NULL) { + g_warning("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(const void *midi_data, size_t len) +{ + smf_event_t *event; + + event = smf_event_new(); + if (event == NULL) + return (NULL); + + event->midi_buffer_length = len; + event->midi_buffer = (uint8_t*)malloc(event->midi_buffer_length); + if (event->midi_buffer == NULL) { + g_warning("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) +{ + size_t len; + + smf_event_t *event; + + event = smf_event_new(); + if (event == NULL) + return (NULL); + + if (first_byte < 0) { + g_warning("First byte of MIDI message cannot be < 0"); + smf_event_delete(event); + + return (NULL); + } + + if (first_byte > 255) { + g_warning("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_warning("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_warning("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_warning("smf_event_new_from_bytes: second byte cannot be a status byte."); + return (NULL); + } + } + + if (len > 2) { + if (third_byte > 255) { + g_warning("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_warning("smf_event_new_from_bytes: third byte cannot be a status byte."); + return (NULL); + } + } + + event->midi_buffer_length = len; + event->midi_buffer = (uint8_t*)malloc(event->midi_buffer_length); + if (event->midi_buffer == NULL) { + g_warning("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_event_remove_from_track(event); + + if (event->midi_buffer != NULL) { + memset(event->midi_buffer, 0, event->midi_buffer_length); + 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) +{ + const 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 = (const smf_event_t *)*(const gpointer *)aa; + b = (const smf_event_t *)*(const gpointer *)bb; + + if (a->time_pulses < b->time_pulses) + return (-1); + + if (a->time_pulses > b->time_pulses) + return (1); + + /* + * We need to preserve original order, otherwise things will break + * when there are several events with the same ->time_pulses. + * XXX: This is an ugly hack; we should remove sorting altogether. + */ + + if (a->event_number < b->event_number) + return (-1); + + if (a->event_number > b->event_number) + 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, size_t 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_event_remove_from_track(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) +{ + size_t i, last_pulses = 0; + + assert(track->smf != NULL); + assert(event->track == NULL); + assert(event->delta_time_pulses == -1); + 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 == 0); + 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); + } + } + + /* Adjust ->delta_time_pulses of the next event. */ + if (event->event_number < track->number_of_events) { + smf_event_t *next_event = smf_track_get_event_by_number(track, event->event_number + 1); + assert(next_event); + assert(next_event->time_pulses >= event->time_pulses); + next_event->delta_time_pulses -= event->delta_time_pulses; + assert(next_event->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, uint32_t 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, size_t 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_event_remove_from_track(smf_event_t *event) +{ + size_t i; + int 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 = 0; + + /* 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 = 0; + event->delta_time_pulses = -1; + event->time_pulses = 0; + 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_warning("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, uint16_t ppqn) +{ + 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; + + /* Track is empty? */ + if (track->number_of_events == 0) + return (NULL); + + /* End of track? */ + if (track->next_event_number == 0) + return (NULL); + + assert(track->next_event_number >= 1); + + 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 = 0; + } + + 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 == 0) + 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, size_t event_number) +{ + smf_event_t *event; + + assert(event_number >= 1); + + if (event_number > track->number_of_events) + return (NULL); + + event = (smf_event_t*)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; + size_t 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 == 0) + 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); +} + +/** + * Advance the "next event counter". This is functionally the same as calling + * smf_get_next_event and ignoring the return value. + */ +void +smf_skip_next_event(smf_t *smf) +{ + smf_event_t *ignored = smf_get_next_event(smf); + (void) ignored; +} + +/** + * \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 = 0; + 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_skip_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_warning("Trying to seek past the end of song."); + return (-1); + } + + if (event->time_seconds < seconds) + smf_skip_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, size_t pulses) +{ + smf_event_t *event; + + smf_rewind(smf); + +#if 0 + g_debug("Seeking to %d pulses.", pulses); +#endif + + for (;;) { + event = smf_peek_next_event(smf); + + if (event == NULL) { + g_warning("Trying to seek past the end of song."); + return (-1); + } + + if (event->time_pulses < pulses) + smf_skip_next_event(smf); + else + break; + } + + smf->last_seek_position = event->time_seconds; + + return (0); +} + +/** + * \return Length of SMF, in pulses. + */ +size_t +smf_get_length_pulses(const smf_t *smf) +{ + int i; + size_t pulses = 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_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/libs/evoral/libsmf/smf.h b/libs/evoral/libsmf/smf.h new file mode 100644 index 0000000000..82647779e8 --- /dev/null +++ b/libs/evoral/libsmf/smf.h @@ -0,0 +1,418 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała <trasz@FreeBSD.org> + * 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. + * + */ + +/** + * \file + * + * Public interface declaration for libsmf, Standard MIDI File format library. + */ + +/** + * + * \page libsmf libsmf - 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. Do not declare static instances + * of these types, i.e. never do something like this: "smf_t smf;". Always use + * "smf_t *smf = smf_new();". The same applies to smf_track_t and smf_event_t. + * + * 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_event_t *event; + * + * smf = smf_load(file_name); + * if (smf == NULL) { + * Whoops, something went wrong. + * return; + * } + * + * while ((event = smf_get_next_event(smf)) != NULL) { + * if (smf_event_is_metadata(event)) + * continue; + * + * wait until event->time_seconds. + * feed_to_midi_output(event->midi_buffer, event->midi_buffer_length); + * } + * + * smf_delete(smf); + * + * \endcode + * + * Saving works like this: + * + * \code + * + * smf_t *smf; + * smf_track_t *track; + * smf_event_t *event; + * + * smf = smf_new(); + * if (smf == NULL) { + * Whoops. + * return; + * } + * + * for (int i = 1; i <= number of tracks; i++) { + * track = smf_track_new(); + * if (track == NULL) { + * Whoops. + * return; + * } + * + * smf_add_track(smf, track); + * + * for (int j = 1; j <= number of events you want to put into this track; j++) { + * 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; + * } + * + * smf_delete(smf); + * + * \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(), smf_seek_to_pulses() + * or smf_seek_to_event()) and then do smf_get_next_event() in loop, until it returns NULL. + * Calling smf_load() causes the smf to be 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 + * + * To create new event, use smf_event_new(), smf_event_new_from_pointer() or + * smf_event_new_from_bytes(). First one creates an empty event - you need to manually allocate + * (using malloc(3)) buffer for MIDI data, write MIDI data into it, put the address of that + * buffer into event->midi_buffer, and the length of MIDI data into event->midi_buffer_length. + * Note that deleting the event (using smf_event_delete()) will free the buffer. + * + * Second form does most of this for you: it takes an address of the buffer containing MIDI data, + * allocates storage and copies MIDI data into it. + * + * Third form is useful for manually creating short events, up to three bytes in length, for + * example Note On or Note Off events. It simply takes three bytes and creates MIDI event + * containing them. If you need to create MIDI message that takes only two bytes, pass -1 as + * the third byte. For one byte message (System Realtime), pass -1 as second and third byte. + * + * To free an event, use smf_event_delete(). + * + * To add event to the track, use smf_track_add_event_delta_pulses(), smf_track_add_event_pulses(), + * or smf_track_add_event_seconds(). The difference between them is in the way you specify the + * time of the event - with the first one, you specify it as an interval, in pulses, from the + * previous event in this track; with the second one, you specify it as pulses from the start + * of the song, and with the last one, you specify it as seconds from the start of the song. + * Obviously, the first version can only append events at the end of the track. + * + * To remove an event from the track it's attached to, use smf_event_remove_from_track(). + * You may want to free the event (using smf_event_delete()) afterwards. + * + * To create new track, use smf_track_new(). To add track to the smf, use smf_add_track(). + * To remove track from its smf, use smf_track_remove_from_smf(). To free the track structure, + * use smf_track_delete(). + * + * Note that libsmf keeps things consistent. If you free (using smf_track_delete()) a track + * that is attached to an smf and contains events, libsmf will detach the events, free them, + * detach the track, free it etc. + * + * Tracks and events are numbered consecutively, starting from one. If you remove a track + * or event, the rest of tracks/events will get renumbered. To get the number of a given + * 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 manually, 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 (using smf_track_add_event_seconds(), smf_track_add_event_pulses() or + * 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 PropertyChange or + * Time Signature, libsmf adds that event to the tempo map. If you remove Tempo PropertyChange 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_event_remove_from_track() function returns. + * Adding Tempo PropertyChange in the middle of the song works in a similar way. + * + * MIDI data (event->midi_buffer) is always kept in normalized form - it always begins 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_". First argument for routines whose names start + * with "smf_event_" is "smf_event_t *", for routines whose names start with "smf_track_" - + * "smf_track_t *", and for plain "smf_" - "smf_t *". The only exception are smf_whatever_new + * routines. 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 its 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, which basically means you can use it freely in your software, + * both Open Source (including GPL) and closed source. + * + */ + +#ifndef SMF_H +#define SMF_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include <stdio.h> +#include <stdint.h> +#include <glib.h> + +#if defined(__GNUC__) && __GNUC__ >= 4 +#define WARN_UNUSED_RESULT __attribute__ ((warn_unused_result)) +#else +#define WARN_UNUSED_RESULT +#endif + +/** Represents a "song", that is, collection of one or more tracks. */ +struct smf_struct { + int format; + + /** These fields are extracted from "division" field of MThd header. + * Valid is _either_ ppqn or frames_per_second/resolution. */ + uint16_t 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; + size_t file_buffer_length; + size_t next_chunk_offset; + int expected_number_of_tracks; + + /** 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; + +/** Describes a single tempo or time signature change. */ +struct smf_tempo_struct { + size_t 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; + +/** Represents a single track. */ +struct smf_track_struct { + smf_t *smf; + + int track_number; + size_t number_of_events; + /* this will be set from the SMF file if present, during loading */ + char* name; + char* instrument; + /** These are private fields using only by loading and saving routines. */ + void *file_buffer; + size_t file_buffer_length; + int last_status; /* Used for "running status". */ + + /** Private, used by smf.c. */ + /** Offset into buffer, used in parse_next_event(). */ + size_t next_event_offset; + size_t next_event_number; + + /** Absolute time of next event on events_queue. */ + size_t time_of_next_event; + GPtrArray *events_array; +}; + +typedef struct smf_track_struct smf_track_t; + +/** Represents a single MIDI event or metaevent. */ +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 1. */ + size_t 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. */ + int32_t delta_time_pulses; + + /** Time, in pulses, since the start of the song. */ + size_t 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. */ + uint8_t *midi_buffer; + + /** Length of the MIDI message in the buffer, in bytes. */ + size_t midi_buffer_length; +}; + +typedef struct smf_event_struct smf_event_t; + +/* Routines for manipulating smf_t. */ +smf_t *smf_new(void) WARN_UNUSED_RESULT; +void smf_delete(smf_t *smf); + +int smf_set_format(smf_t *smf, int format) WARN_UNUSED_RESULT; +int smf_set_ppqn(smf_t *smf, uint16_t ppqn) WARN_UNUSED_RESULT; + +char *smf_decode(const smf_t *smf) WARN_UNUSED_RESULT; + +smf_track_t *smf_get_track_by_number(const smf_t *smf, int track_number) WARN_UNUSED_RESULT; + +smf_event_t *smf_peek_next_event(smf_t *smf) WARN_UNUSED_RESULT; +smf_event_t *smf_get_next_event(smf_t *smf) WARN_UNUSED_RESULT; +void smf_skip_next_event(smf_t *smf); + +void smf_rewind(smf_t *smf); +int smf_seek_to_seconds(smf_t *smf, double seconds) WARN_UNUSED_RESULT; +int smf_seek_to_pulses(smf_t *smf, size_t pulses) WARN_UNUSED_RESULT; +int smf_seek_to_event(smf_t *smf, const smf_event_t *event) WARN_UNUSED_RESULT; + +size_t smf_get_length_pulses(const smf_t *smf) WARN_UNUSED_RESULT; +double smf_get_length_seconds(const smf_t *smf) WARN_UNUSED_RESULT; +int smf_event_is_last(const smf_event_t *event) WARN_UNUSED_RESULT; + +void smf_add_track(smf_t *smf, smf_track_t *track); +void smf_track_remove_from_smf(smf_track_t *track); + +/* Routines for manipulating smf_track_t. */ +smf_track_t *smf_track_new(void) WARN_UNUSED_RESULT; +void smf_track_delete(smf_track_t *track); + +smf_event_t *smf_track_get_next_event(smf_track_t *track) WARN_UNUSED_RESULT; +smf_event_t *smf_track_get_event_by_number(const smf_track_t *track, size_t num) WARN_UNUSED_RESULT; +smf_event_t *smf_track_get_last_event(const smf_track_t *track) WARN_UNUSED_RESULT; + +void smf_track_add_event_delta_pulses(smf_track_t *track, smf_event_t *event, uint32_t delta); +void smf_track_add_event_pulses(smf_track_t *track, smf_event_t *event, size_t 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, uint32_t delta) WARN_UNUSED_RESULT; +int smf_track_add_eot_pulses(smf_track_t *track, size_t pulses) WARN_UNUSED_RESULT; +int smf_track_add_eot_seconds(smf_track_t *track, double seconds) WARN_UNUSED_RESULT; +void smf_event_remove_from_track(smf_event_t *event); + +/* Routines for manipulating smf_event_t. */ +smf_event_t *smf_event_new(void) WARN_UNUSED_RESULT; +smf_event_t *smf_event_new_from_pointer(const void *midi_data, size_t len) WARN_UNUSED_RESULT; +smf_event_t *smf_event_new_from_bytes(int byte1, int byte2, int byte3) WARN_UNUSED_RESULT; +smf_event_t *smf_event_new_textual(int type, const char *text); +void smf_event_delete(smf_event_t *event); + +int smf_event_is_valid(const smf_event_t *event) WARN_UNUSED_RESULT; +int smf_event_is_metadata(const smf_event_t *event) WARN_UNUSED_RESULT; +int smf_event_is_system_realtime(const smf_event_t *event) WARN_UNUSED_RESULT; +int smf_event_is_system_common(const smf_event_t *event) WARN_UNUSED_RESULT; +int smf_event_is_sysex(const smf_event_t *event) WARN_UNUSED_RESULT; +int smf_event_is_eot(const smf_event_t *event) WARN_UNUSED_RESULT; +int smf_event_is_textual(const smf_event_t *event) WARN_UNUSED_RESULT; +char *smf_event_decode(const smf_event_t *event) WARN_UNUSED_RESULT; +char *smf_event_extract_text(const smf_event_t *event) WARN_UNUSED_RESULT; + +/* Routines for dealing with Variable Length Quantities (VLQ's). + Slightly odd names reflect original static names within libsmf + */ +int smf_format_vlq (unsigned char *buf, int length, unsigned long value); +int smf_extract_vlq(const unsigned char *buf, const size_t buffer_length, uint32_t *value, uint32_t *len); + +/* Routines for loading SMF files. */ +smf_t *smf_load(FILE *) WARN_UNUSED_RESULT; +smf_t *smf_load_from_memory(void *buffer, const size_t buffer_length) WARN_UNUSED_RESULT; + +/* Routine for writing SMF files. */ +int smf_save(smf_t *smf, FILE *file) WARN_UNUSED_RESULT; + +/* Routines for manipulating smf_tempo_t. */ +smf_tempo_t *smf_get_tempo_by_pulses(const smf_t *smf, size_t pulses) WARN_UNUSED_RESULT; +smf_tempo_t *smf_get_tempo_by_seconds(const smf_t *smf, double seconds) WARN_UNUSED_RESULT; +int smf_get_tempo_count (const smf_t *smf) WARN_UNUSED_RESULT; +smf_tempo_t *smf_get_tempo_by_number(const smf_t *smf, size_t number) WARN_UNUSED_RESULT; +smf_tempo_t *smf_get_last_tempo(const smf_t *smf) WARN_UNUSED_RESULT; + +const char *smf_get_version(void) WARN_UNUSED_RESULT; + +#ifdef __cplusplus +} +#endif + +#endif /* SMF_H */ + diff --git a/libs/evoral/libsmf/smf_decode.c b/libs/evoral/libsmf/smf_decode.c new file mode 100644 index 0000000000..4bd774f345 --- /dev/null +++ b/libs/evoral/libsmf/smf_decode.c @@ -0,0 +1,646 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała <trasz@FreeBSD.org> + * 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. + * + */ + +/** + * \file + * + * Event decoding routines. + * + */ + +#include <stdlib.h> +#include <string.h> +#include <assert.h> +#include <math.h> +#include <errno.h> +#ifdef PLATFORM_WINDOWS +#include <winsock2.h> +#else +#include <arpa/inet.h> +#endif +#include <stdint.h> +#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 = (char*)malloc(BUFFER_SIZE); + if (buf == NULL) { + g_warning("smf_event_decode_textual: malloc failed."); + return (NULL); + } + + extracted = smf_event_extract_text(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 = (char*)malloc(BUFFER_SIZE); + if (buf == NULL) { + g_warning("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_warning("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_warning("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_warning("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_warning("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((double)2, event->midi_buffer[4]), event->midi_buffer[5], + event->midi_buffer[6]); + break; + + case 0x59: + if (event->midi_buffer_length < 5) { + g_warning("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_warning("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 && (size_t)i < sizeof(minor_keys) / sizeof(*minor_keys)); + assert(i >= 0 && (size_t)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 %" G_GSIZE_FORMAT, + event->midi_buffer_length); + break; + + default: + goto error; + } + + assert (off <= BUFFER_SIZE); + 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_warning("smf_event_decode_system_realtime: event length is not 1."); + return (NULL); + } + + buf = (char*)malloc(BUFFER_SIZE); + if (buf == NULL) { + g_warning("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); + } + + assert (off <= BUFFER_SIZE); + 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_warning("smf_event_decode_sysex: truncated MIDI message."); + return (NULL); + } + + buf = (char*)malloc(BUFFER_SIZE); + if (buf == NULL) { + g_warning("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); + + assert (off <= BUFFER_SIZE); + 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"); + + assert (off <= BUFFER_SIZE); + 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 = (char*)malloc(BUFFER_SIZE); + if (buf == NULL) { + g_warning("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); + } + + assert (off <= BUFFER_SIZE); + return (buf); +} + +static void +note_from_int(char *buf, int note_number) +{ + int note, octave; + const 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 1, note F#3, velocity 0 + * + * You should free the returned string afterwards, using free(3). + */ +char * +smf_event_decode(const smf_event_t *event) +{ + int off = 0, channel; + 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_warning("smf_event_decode: incorrect MIDI message length."); + return (NULL); + } + + buf = (char*)malloc(BUFFER_SIZE); + if (buf == NULL) { + g_warning("smf_event_decode: malloc failed."); + return (NULL); + } + + /* + 1, because user-visible channels used to be in range <1-16>. */ + channel = (event->midi_buffer[0] & 0x0F) + 1; + + 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", + channel, 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", + channel, 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", + channel, note, event->midi_buffer[2]); + break; + + case 0xB0: + off += snprintf(buf + off, BUFFER_SIZE - off, "Controller, channel %d, controller %d, value %d", + channel, event->midi_buffer[1], event->midi_buffer[2]); + break; + + case 0xC0: + off += snprintf(buf + off, BUFFER_SIZE - off, "Program Change, channel %d, controller %d", + channel, event->midi_buffer[1]); + break; + + case 0xD0: + off += snprintf(buf + off, BUFFER_SIZE - off, "Channel Pressure, channel %d, pressure %d", + channel, event->midi_buffer[1]); + break; + + case 0xE0: + off += snprintf(buf + off, BUFFER_SIZE - off, "Pitch Wheel, channel %d, value %d", + channel, ((int)event->midi_buffer[2] << 7) | (int)event->midi_buffer[2]); + break; + + default: + free(buf); + return (NULL); + } + + assert(off <= BUFFER_SIZE); + + 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. + * + * You should free the returned string afterwards, using free(3). + */ +char * +smf_decode(const smf_t *smf) +{ + int off = 0; + char *buf; + + buf = (char*)malloc(BUFFER_SIZE); + if (buf == NULL) { + g_warning("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); + + assert (off <= BUFFER_SIZE); + return (buf); +} + diff --git a/libs/evoral/libsmf/smf_load.c b/libs/evoral/libsmf/smf_load.c new file mode 100644 index 0000000000..f8b4158004 --- /dev/null +++ b/libs/evoral/libsmf/smf_load.c @@ -0,0 +1,984 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała <trasz@FreeBSD.org> + * 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. + * + */ + +/** + * \file + * + * Standard MIDI File format loader. + * + */ + +/* Reference: http://www.borg.com/~jglatt/tech/midifile.htm */ + +#include <stdlib.h> +#include <string.h> +#include <assert.h> +#include <math.h> +#include <errno.h> +#include <ctype.h> +#ifdef PLATFORM_WINDOWS +#include <winsock2.h> +#else +#include <arpa/inet.h> +#endif +#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); + + if (smf->next_chunk_offset + sizeof(struct chunk_header_struct) >= smf->file_buffer_length) { + g_warning("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_warning("SMF error: chunk signature contains at least one non-alphanumeric byte."); + return (NULL); + } + + /* + * XXX: On SPARC, after compiling with "-fast" option there will be SIGBUS here. + * Please compile with -xmemalign=8i". + */ + smf->next_chunk_offset += sizeof(struct chunk_header_struct) + ntohl(chunk->length); + + if (smf->next_chunk_offset > smf->file_buffer_length) { + g_warning("SMF error: malformed chunk; truncated file?"); + } + + 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_warning ("SMF error: file is too short, it cannot be a MIDI file."); + + return (-1); + } + + tmp_mthd = (struct chunk_header_struct*)smf->file_buffer; + + if (!chunk_signature_matches(tmp_mthd, "MThd")) { + g_warning("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_warning("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_warning("SMF error: bad MThd format field value: %d, valid values are 0-2, inclusive.", smf->format); + return (-1); + } + + if (smf->format == 2) { + g_warning("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_warning("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_warning("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. + */ +int +smf_extract_vlq(const unsigned char *buf, const size_t buffer_length, uint32_t *value, uint32_t *len) +{ + uint32_t val = 0; + const unsigned char *c = buf; + int i = 0; + + for (;; ++i) { + if (c >= buf + buffer_length) { + g_warning("End of buffer in extract_vlq()."); + return (-1); + } + + if (i == 4 && (val & 0xfe000000)) { + g_warning("SMF error: Variable Length Quantities longer than four bytes are not supported yet."); + return (-2); + } + + val = (val << 7) + (*c & 0x7F); + + if (*c & 0x80) + c++; + else + break; + }; + + assert(c >= buf); + *value = val; + *len = c - buf + 1; + + if (*len > 5) { + g_warning("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 int32_t +expected_sysex_length(const unsigned char status, const unsigned char *second_byte, const size_t buffer_length, int32_t *consumed_bytes) +{ + uint32_t sysex_length = 0; + uint32_t len = 0; + +#ifndef NDEBUG + (void) status; +#else + assert(status == 0xF0); +#endif + + if (buffer_length < 3) { + g_warning("SMF error: end of buffer in expected_sysex_length()."); + return (-1); + } + + smf_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 int32_t +expected_escaped_length(const unsigned char status, const unsigned char *second_byte, const size_t buffer_length, int32_t *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 int32_t +expected_message_length(unsigned char status, const unsigned char *second_byte, const size_t 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)); + + /* Is this a metamessage? */ + if (status == 0xFF) { + if (buffer_length < 2) { + g_warning("SMF error: end of buffer in expected_message_length()."); + return (-1); + } + + /* + * Format of this kind of messages is like this: 0xFF 0xTYPE 0xlength and then "length" bytes. + * TYPE is < 127, length may be 0 + * + * "lenght" is a 7bit value, the 8th bit is used to extend the length. + * eg. ff02 8266 <0x166 byte (C) message follows> + */ + int32_t mlen = 0; + int32_t off; + for (off = 1; off < 4; ++off) { + uint8_t val = *(second_byte + off); + mlen = mlen << 7 | (val & 0x7f); + if (0 == (val & 0x80)) { + mlen += 2 + off; // 2 byte "header" 0xff <type> + <length of length> + break; + } + } + return mlen; + } + + 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_warning("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_warning("SMF error: unknown status byte '0x%x'.", status); + return (-3); + } +} + +static int +extract_sysex_event(const unsigned char *buf, const size_t buffer_length, smf_event_t *event, uint32_t *len, int last_status) +{ + (void) last_status; + + int status; + int32_t vlq_length, message_length; + const unsigned char *c = buf; + + status = *buf; + + if (!(is_sysex_byte(status))) { + g_warning("Corrupt sysex status byte in extract_sysex_event()."); + return (-6); + } + + 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 + (size_t)message_length >= buffer_length) { + g_warning("End of buffer in extract_sysex_event()."); + return (-5); + } + + event->midi_buffer_length = message_length; + event->midi_buffer = (uint8_t*)malloc(event->midi_buffer_length); + if (event->midi_buffer == NULL) { + g_warning("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 size_t buffer_length, smf_event_t *event, uint32_t *len, int last_status) +{ + (void) last_status; + + int status; + int32_t message_length = 0; + int32_t vlq_length = 0; + const unsigned char *c = buf; + + status = *buf; + + if (!(is_escape_byte(status))) { + g_warning("Corrupt escape status byte in extract_escaped_event()."); + return (-6); + } + + 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 + (size_t)message_length >= buffer_length) { + g_warning("End of buffer in extract_escaped_event()."); + return (-5); + } + + event->midi_buffer_length = message_length; + event->midi_buffer = (uint8_t*)malloc(event->midi_buffer_length); + if (event->midi_buffer == NULL) { + g_warning("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_warning("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 size_t buffer_length, smf_event_t *event, uint32_t *len, int last_status) +{ + int status; + int32_t 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_warning("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 ((size_t)message_length > buffer_length - (c - buf) + 1) { + g_warning("End of buffer in extract_midi_event()."); + return (-5); + } + + event->midi_buffer_length = message_length; + event->midi_buffer = (uint8_t*)malloc(event->midi_buffer_length); + if (event->midi_buffer == NULL) { + g_warning("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) +{ + uint32_t etime = 0; + uint32_t len; + size_t 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; + /* if there was no meta-EOT event, buffer_length can be zero. This is + an error in the SMF file, but it shouldn't be treated as fatal. + */ + if (buffer_length == 0) { + g_warning ("SMF warning: expected EOT at end of track, but none found"); + goto error; + } + /* First, extract time offset from previous event. */ + if (smf_extract_vlq(c, buffer_length, &etime, &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, etime); + + 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 size_t buffer_length, uint32_t len) +{ + char *str; + + assert(buffer_length > 0); + assert(len > 0); + + if (len > buffer_length) { + g_warning("End of buffer in make_string()."); + + len = buffer_length; + } + + str = (char*)malloc(len + 1); + if (str == NULL) { + g_warning("Cannot allocate memory in make_string()."); + return (NULL); + } + + memcpy(str, buf, len); + str[len] = '\0'; + + return (str); +} + +/** + * \return 1, if passed a metaevent containing text, that is, Text, Copyright, + * Sequence/Track Name, Instrument, Lyric, Marker, Cue Point, Program Name, + * or Device Name; 0 otherwise. + */ +int +smf_event_is_textual(const smf_event_t *event) +{ + if (!smf_event_is_metadata(event)) { + return (0); + } + + if (event->midi_buffer_length < 4) { + return (0); + } + + if (event->midi_buffer[1] < 1 || event->midi_buffer[1] > 7) { + return (0); + } + + return (1); +} + +/** + * Extracts text from "textual metaevents", such as Text or Lyric. + * + * \return Zero-terminated string extracted from "text events" or NULL, if there was any problem. + */ +char * +smf_event_extract_text(const smf_event_t *event) +{ + uint32_t string_length = 0; + uint32_t length_length = 0; + + if (!smf_event_is_textual(event)) { + g_warning ("smf_event_extract_text: event is not textual."); + return (NULL); + } + + smf_extract_vlq((const unsigned char*)(void *)&(event->midi_buffer[2]), event->midi_buffer_length - 2, &string_length, &length_length); + + if (string_length <= 0) { + g_warning("smf_event_extract_text: truncated MIDI message."); + return (NULL); + } + + return (make_string((const unsigned char*)(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); + + int32_t expected; + + if (event->midi_buffer_length < 1) + return (0); + + /* We cannot use expected_message_length on sysexes. */ + if (smf_event_is_sysex(event)) + return (1); + + + expected = expected_message_length(event->midi_buffer[0], + &(event->midi_buffer[1]), event->midi_buffer_length - 1); + if (expected < 0 || event->midi_buffer_length != (size_t)expected) { + 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_warning("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; + int ret = 0; + + if (parse_mtrk_header(track)) + return (-1); + + for (;;) { + event = parse_next_event(track); + + /* Couldn't parse an event? */ + if (event == NULL || !smf_event_is_valid(event)) { + ret = -1; + break; + } + + if (event_is_end_of_track(event)) { + break; + } + + if (smf_event_is_metadata (event)) { + switch (event->midi_buffer[1]) { + case 0x03: + track->name = smf_event_extract_text (event); + break; + case 0x04: + track->instrument = smf_event_extract_text (event); + break; + default: + break; + } + } + } + + track->file_buffer = NULL; + track->file_buffer_length = 0; + track->next_event_offset = -1; + + return (ret); +} + +/** + * Allocate buffer of proper size and read file contents into it. + */ +static int +load_file_into_buffer(void **file_buffer, size_t *file_buffer_length, FILE* stream) +{ + long offset; + + if (stream == NULL) { + g_warning("Cannot open input file: %s", strerror(errno)); + + return (-1); + } + + if (fseek(stream, 0, SEEK_END)) { + g_warning("fseek(3) failed: %s", strerror(errno)); + + return (-2); + } + + offset = ftell(stream); + if (offset < 0) { + g_warning("ftell(3) failed: %s", strerror(errno)); + + return (-3); + } + *file_buffer_length = (size_t)offset; + + if (fseek(stream, 0, SEEK_SET)) { + g_warning("fseek(3) failed: %s", strerror(errno)); + + return (-4); + } + + *file_buffer = malloc(*file_buffer_length); + if (*file_buffer == NULL) { + g_warning("malloc(3) failed: %s", strerror(errno)); + + return (-5); + } + + if (fread(*file_buffer, 1, *file_buffer_length, stream) != *file_buffer_length) { + g_warning("fread(3) failed: %s", strerror(errno)); + free (*file_buffer); + *file_buffer = NULL; + return (-6); + } + + 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(void *buffer, const size_t buffer_length) +{ + int i; + int ret; + + 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); + + ret = parse_mtrk_chunk(track); + + track->file_buffer = NULL; + track->file_buffer_length = 0; + track->next_event_offset = -1; + + if (ret) { + g_warning("SMF warning: Error parsing track, continuing with data loaded so far."); + break; + } + } + + 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 = 0; + + return (smf); +} + +/** + * Loads SMF file. + * + * \param file Open file. + * \return SMF or NULL, if loading failed. + */ +smf_t * +smf_load(FILE *file) +{ + size_t file_buffer_length; + void *file_buffer; + smf_t *smf; + + if (load_file_into_buffer(&file_buffer, &file_buffer_length, file)) + return (NULL); + + smf = smf_load_from_memory(file_buffer, file_buffer_length); + + memset(file_buffer, 0, file_buffer_length); + free(file_buffer); + + if (smf == NULL) + return (NULL); + + smf_rewind(smf); + + return (smf); +} + diff --git a/libs/evoral/libsmf/smf_private.h b/libs/evoral/libsmf/smf_private.h new file mode 100644 index 0000000000..41ac7238fc --- /dev/null +++ b/libs/evoral/libsmf/smf_private.h @@ -0,0 +1,81 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała <trasz@FreeBSD.org> + * 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 <stdint.h> +#include <sys/types.h> + +//#include "config.h" +//#define SMF_VERSION PACKAGE_VERSION + +/** + * \file + * + * Private header. Applications using libsmf should use smf.h. + * + */ + +#if defined(__GNUC__) +#define ATTRIBUTE_PACKED __attribute__((__packed__)) +#else +#define ATTRIBUTE_PACKED +#pragma pack(1) +#endif + +/** SMF chunk header, used only by smf_load.c and smf_save.c. */ +struct chunk_header_struct { + char id[4]; + uint32_t length; +} ATTRIBUTE_PACKED; + +/** SMF chunk, used only by smf_load.c and smf_save.c. */ +struct mthd_chunk_struct { + struct chunk_header_struct mthd_header; + uint16_t format; + uint16_t number_of_tracks; + uint16_t division; +} ATTRIBUTE_PACKED; + +#if (!defined __GNUC__) +#pragma pack() +#endif + +void smf_track_add_event(smf_track_t *track, smf_event_t *event); +void smf_init_tempo(smf_t *smf); +void smf_fini_tempo(smf_t *smf); +void 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, size_t pulses); +int smf_event_is_tempo_change_or_time_signature(const smf_event_t *event) WARN_UNUSED_RESULT; +int smf_event_length_is_valid(const smf_event_t *event) WARN_UNUSED_RESULT; +int is_status_byte(const unsigned char status) WARN_UNUSED_RESULT; +smf_track_t* smf_find_track_with_next_event (smf_t *smf); + +#endif /* SMF_PRIVATE_H */ + diff --git a/libs/evoral/libsmf/smf_save.c b/libs/evoral/libsmf/smf_save.c new file mode 100644 index 0000000000..12eb879c4e --- /dev/null +++ b/libs/evoral/libsmf/smf_save.c @@ -0,0 +1,691 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała <trasz@FreeBSD.org> + * 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. + * + */ + +/** + * \file + * + * Standard MIDI File writer. + * + */ + +/* Reference: http://www.borg.com/~jglatt/tech/midifile.htm */ + +#include <stdlib.h> +#include <string.h> +#include <assert.h> +#include <math.h> +#include <errno.h> +#ifdef PLATFORM_WINDOWS +#include <winsock2.h> +#else +#include <arpa/inet.h> +#endif +#include "smf.h" +#include "smf_private.h" + +#define MAX_VLQ_LENGTH 128 + +/** + * 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 = (char*)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_warning("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_warning("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_warning("Cannot extend track buffer."); + return (-1); + } + + memcpy(dest, buffer, buffer_length); + + return (0); +} + +int +smf_format_vlq(unsigned char *buf, int length, unsigned long value) +{ + int i; + unsigned long buffer; + + /* Taken from http://www.borg.com/~jglatt/tech/midifile/vari.htm */ + buffer = value & 0x7F; + + while ((value >>= 7)) { + buffer <<= 8; + buffer |= ((value & 0x7F) | 0x80); + } + + for (i = 0;; i++) { + buf[i] = buffer; + + if (buffer & 0x80) + buffer >>= 8; + else + break; + } + + assert(i <= length); + + /* + 1, because "i" is an offset, not a count. */ + return (i + 1); +} + +smf_event_t * +smf_event_new_textual(int type, const char *text) +{ + int vlq_length, text_length, copied_length; + smf_event_t *event; + + assert(type >= 1 && type <= 9); + + text_length = strlen(text); + + event = smf_event_new(); + if (event == NULL) + return (NULL); + + /* "2 +" is for leading 0xFF 0xtype. */ + event->midi_buffer_length = 2 + text_length + MAX_VLQ_LENGTH; + event->midi_buffer = (uint8_t*)malloc(event->midi_buffer_length); + if (event->midi_buffer == NULL) { + g_warning("Cannot allocate MIDI buffer structure: %s", strerror(errno)); + smf_event_delete(event); + + return (NULL); + } + + event->midi_buffer[0] = 0xFF; + event->midi_buffer[1] = type; + + vlq_length = smf_format_vlq(event->midi_buffer + 2, MAX_VLQ_LENGTH - 2, text_length); + copied_length = snprintf((char *)event->midi_buffer + vlq_length + 2, event->midi_buffer_length - vlq_length - 2, "%s", text); + +#ifndef NDEBUG + (void) copied_length; /* stop gcc warning about unusued vars for non-debug build */ +#else + assert(copied_length == text_length); +#endif + + event->midi_buffer_length = 2 + vlq_length + text_length; + + return event; +} + +/** + * Appends value, expressed as Variable Length Quantity, to event->track. + */ +static int +write_vlq(smf_event_t *event, unsigned long value) +{ + unsigned char buf[MAX_VLQ_LENGTH]; + int vlq_length; + + vlq_length = smf_format_vlq(buf, MAX_VLQ_LENGTH, value); + + return (track_append(event->track, buf, vlq_length)); +} + +/** + * 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); + + if (track->name) { + printf ("save track name [%s]\n", track->name); + smf_event_t *ev = smf_event_new_textual (0x03, track->name); + ev->delta_time_pulses = 0; /* time zero */ + ev->track = track; + ret = write_event (ev); + ev->track = 0; + smf_event_delete (ev); + if (ret) { + return ret; + } + } + + if (track->instrument) { + printf ("save track instrument [%s]\n", track->instrument); + smf_event_t *ev = smf_event_new_textual (0x04, track->instrument); + ev->delta_time_pulses = 0; /* time zero */ + ev->track = track; + ret = write_event (ev); + ev->track = 0; + smf_event_delete (ev); + 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. + */ +static int +write_file(smf_t *smf, FILE* stream) +{ + if (fwrite(smf->file_buffer, 1, smf->file_buffer_length, stream) != smf->file_buffer_length) { + g_warning("fwrite(3) failed: %s", strerror(errno)); + + return (-2); + } + + return (0); +} + +static void +free_buffer(smf_t *smf) +{ + int i; + smf_track_t *track; + + /* Clear the pointers. */ + memset(smf->file_buffer, 0, smf->file_buffer_length); + 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; + } +} + +#ifndef NDEBUG + +/** + * \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; + if (smf->file_buffer != NULL) { + fprintf (stderr, "SFB != null but == %p\n", smf->file_buffer); + } + 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); +} + +#endif /* !NDEBUG */ + +/** + * \return Nonzero, if event is End Of Track metaevent. + */ +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, eot_found; + size_t eventno; + smf_track_t *track; + smf_event_t *event; + + if (smf->format < 0 || smf->format > 2) { + g_warning("SMF error: smf->format is less than zero of greater than two."); + return (-1); + } + + if (smf->number_of_tracks < 1) { + g_warning("SMF error: number of tracks is less than one."); + return (-2); + } + + if (smf->format == 0 && smf->number_of_tracks > 1) { + g_warning("SMF error: format is 0, but number of tracks is more than one."); + return (-3); + } + + if (smf->ppqn <= 0) { + g_warning("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_warning("Event #%" G_GSIZE_FORMAT " on track #%d is invalid.", eventno, trackno); + return (-5); + } + + if (smf_event_is_eot(event)) { + if (eot_found) { + g_warning("Duplicate End Of Track event on track #%d.", trackno); + return (-6); + } + + eot_found = 1; + } + } + + if (!eot_found) { + if (smf_track_add_eot_delta_pulses(track, 0)) { + g_warning("smf_track_add_eot_delta_pulses failed."); + return (-6); + } + } + + } + + return (0); +} + +#ifndef NDEBUG + +#define CHECK(cond) if (!(cond)) { return -1; } + +static int +check_smf_event_is_identical(const smf_event_t *a, const smf_event_t *b) +{ + CHECK(a->event_number == b->event_number); + CHECK(a->delta_time_pulses == b->delta_time_pulses); + CHECK(abs((long)(a->time_pulses - b->time_pulses)) <= 2); + CHECK(fabs(a->time_seconds - b->time_seconds) <= 0.01); + CHECK(a->track_number == b->track_number); + CHECK(a->midi_buffer_length == b->midi_buffer_length); + CHECK(memcmp(a->midi_buffer, b->midi_buffer, a->midi_buffer_length) == 0); + return 0; +} + +static int +check_smf_track_is_identical(const smf_track_t *a, const smf_track_t *b) +{ + size_t i; + + CHECK(a->track_number == b->track_number); + CHECK(a->number_of_events == b->number_of_events); + + for (i = 1; i <= a->number_of_events; i++) + check_smf_event_is_identical(smf_track_get_event_by_number(a, i), smf_track_get_event_by_number(b, i)); + + return 0; +} + +static int +check_smf_is_identical(const smf_t *a, const smf_t *b) +{ + int i; + + CHECK(a->format == b->format); + CHECK(a->ppqn == b->ppqn); + CHECK(a->frames_per_second == b->frames_per_second); + CHECK(a->resolution == b->resolution); + CHECK(a->number_of_tracks == b->number_of_tracks); + + for (i = 1; i <= a->number_of_tracks; i++) + check_smf_track_is_identical(smf_get_track_by_number(a, i), smf_get_track_by_number(b, i)); + + /* We do not need to compare tempos explicitly, as tempo is always computed from track contents. */ + return 0; +} + +static int +check_smf_saved_correctly(const smf_t *smf, FILE* file) +{ + smf_t *saved; + int ret; + + saved = smf_load (file); + if (!saved) { + ret = -1; + } else { + ret = check_smf_is_identical(smf, saved); + } + + smf_delete(saved); + return (ret); +} + +#endif /* !NDEBUG */ + +/** + * Writes the contents of SMF to the file given. + * \param smf SMF. + * \param file File descriptor. + * \return 0, if saving was successfull. + */ +int +smf_save(smf_t *smf, FILE* file) +{ + int i, error; + 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); + + error = write_track(track); + if (error) { + free_buffer(smf); + return (error); + } + } + + error = write_file(smf, file); + + free_buffer(smf); + + if (error) + return (error); + +#ifndef NDEBUG + if (check_smf_saved_correctly(smf, file)) { + g_warning("SMF warning: Did not save correctly, possible data loss."); + } +#endif + + return (0); +} + diff --git a/libs/evoral/libsmf/smf_tempo.c b/libs/evoral/libsmf/smf_tempo.c new file mode 100644 index 0000000000..eaa1b0ef34 --- /dev/null +++ b/libs/evoral/libsmf/smf_tempo.c @@ -0,0 +1,454 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała <trasz@FreeBSD.org> + * 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. + * + */ + +/** + * \file + * + * Tempo map related part. + * + */ + +#include <stdlib.h> +#include <assert.h> +#include <math.h> +#include <string.h> +#include "smf.h" +#include "smf_private.h" + +static double seconds_from_pulses(const smf_t *smf, size_t 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, size_t 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 = (smf_tempo_t*)malloc(sizeof(smf_tempo_t)); + if (tempo == NULL) { + g_warning("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); +} + +/** + * \internal + */ +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 ntempo = (event->midi_buffer[3] << 16) + (event->midi_buffer[4] << 8) + event->midi_buffer[5]; + if (ntempo <= 0) { + g_warning("Ignoring invalid tempo change."); + return; + } + + add_tempo(event->track->smf, event->time_pulses, ntempo); + } + + /* Time Signature? */ + if (event->midi_buffer[1] == 0x58) { + int numerator, denominator, clocks_per_click, notes_per_note; + + if (event->midi_buffer_length < 7) { + g_warning("Time Signature event seems truncated."); + return; + } + + numerator = event->midi_buffer[3]; + denominator = (int)pow((double)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; +} + +/** + * \internal + * + * 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, size_t pulses) +{ + smf_tempo_t *tempo; + + /* XXX: This is a partial 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; + + memset(tempo, 0, sizeof(smf_tempo_t)); + free(tempo); + + g_ptr_array_remove_index(smf->tempo_array, smf->tempo_array->len - 1); +} + +static double +seconds_from_pulses(const smf_t *smf, size_t 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); +} + +/** + * \internal + * + * Computes value of event->time_seconds for all events in smf. + * Warning: rewinds the smf. + */ +void +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; + + maybe_add_to_tempo_map(event); + + event->time_seconds = seconds_from_pulses(smf, event->time_pulses); + } + + /* Not reached. */ +} + +int +smf_get_tempo_count (const smf_t *smf) +{ + if (!smf->tempo_array) { + return 0; + } + + return smf->tempo_array->len; +} + +smf_tempo_t * +smf_get_tempo_by_number(const smf_t *smf, size_t number) +{ + if (number >= smf->tempo_array->len) + return (NULL); + + return ((smf_tempo_t*)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, size_t pulses) +{ + size_t i; + smf_tempo_t *tempo; + + if (pulses == 0) + return (smf_get_tempo_by_number(smf, 0)); + + assert(smf->tempo_array != NULL); + + for (i = smf->tempo_array->len; i > 0; i--) { + tempo = smf_get_tempo_by_number(smf, i - 1); + + 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) +{ + size_t 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; i > 0; i--) { + tempo = smf_get_tempo_by_number(smf, i - 1); + + 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; + + assert(smf->tempo_array->len > 0); + tempo = smf_get_tempo_by_number(smf, smf->tempo_array->len - 1); + assert(tempo); + + return (tempo); +} + +/** + * \internal + * + * Remove all smf_tempo_t structures from SMF. + */ +void +smf_fini_tempo(smf_t *smf) +{ + smf_tempo_t *tempo; + + while (smf->tempo_array->len > 0) { + tempo = (smf_tempo_t*)g_ptr_array_index(smf->tempo_array, smf->tempo_array->len - 1); + assert(tempo); + + memset(tempo, 0, sizeof(smf_tempo_t)); + free(tempo); + + g_ptr_array_remove_index(smf->tempo_array, smf->tempo_array->len - 1); + } + + assert(smf->tempo_array->len == 0); +} + +/** + * \internal + * + * Remove any existing tempos and add default one. + * + * \bug This will abort (by calling g_warning) if new_tempo() (memory allocation there) fails. + */ +void +smf_init_tempo(smf_t *smf) +{ + smf_tempo_t *tempo; + + smf_fini_tempo(smf); + + tempo = new_tempo(smf, 0); + if (tempo == NULL) { + g_warning("tempo_init failed, sorry."); + } +} + +/** + * 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); + + 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, uint32_t delta) +{ + assert(event->time_seconds == -1.0); + assert(track->smf != NULL); + + if (!smf_event_is_valid(event)) { + g_warning("Added event is invalid"); + } + + 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, size_t pulses) +{ + 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_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/libs/evoral/libsmf/smfsh.c b/libs/evoral/libsmf/smfsh.c new file mode 100644 index 0000000000..493c664bd2 --- /dev/null +++ b/libs/evoral/libsmf/smfsh.c @@ -0,0 +1,1034 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała <trasz@FreeBSD.org> + * 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. + * + */ + +/** + * \file + * + * "SMF shell", command line utility. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <sysexits.h> +#include <string.h> +#include <ctype.h> +#include <assert.h> +#include "smf.h" +#include "config.h" + +#ifdef HAVE_LIBREADLINE +#include <readline/readline.h> +#include <readline/history.h> +#endif + +smf_track_t *selected_track = NULL; +smf_event_t *selected_event = NULL; +smf_t *smf = NULL; +char *last_file_name = NULL; + +#define COMMAND_LENGTH 10 + +static void +log_handler(const gchar *log_domain, GLogLevelFlags log_level, const gchar *message, gpointer notused) +{ + if (strcmp(log_domain, "smfsh") == 0) + fprintf(stderr, "%s\n", message); + else + fprintf(stderr, "%s: %s\n", log_domain, message); +} + +static int cmd_track(char *arg); + +static int +cmd_load(char *file_name) +{ + char *decoded; + + if (file_name == NULL) { + if (last_file_name == NULL) { + g_critical("Please specify file name."); + return (-1); + } + + file_name = strdup(last_file_name); + } else { + file_name = strdup(file_name); + } + + selected_track = NULL; + selected_event = NULL; + + if (smf != NULL) { + smf_delete(smf); + smf = NULL; + } + + if (last_file_name != NULL) + free(last_file_name); + 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); + decoded = smf_decode(smf); + g_message("%s.", decoded); + free(decoded); + + cmd_track("1"); + + free(file_name); + + 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 = strdup(last_file_name); + } else { + file_name = strdup(file_name); + } + + if (last_file_name != NULL) + free(last_file_name); + 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); + + free(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); + } + + if (selected_track->number_of_events == 0) { + g_message("Selected track is empty."); + return (0); + } + + 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 <number>')."); + 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: add <time-in-seconds> <midi-in-hex> - for example, 'add 1 903C7F' will add"); + g_message("Note On event, note C4, velocity 127, channel 1, one second from the start of song, channel 1."); +} + +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, using 'track <number>' command."); + return (-1); + } + + if (str == NULL) { + eventadd_usage(); + return (-2); + } + + /* Extract the time. Don't use strsep(3), it doesn't work on SunOS. */ + time = str; + str = strchr(str, ' '); + if (str != NULL) { + *str = '\0'; + 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_text(char *str) +{ + double seconds, type; + char *time, *typestr, *end; + + if (selected_track == NULL) { + g_critical("Please select a track first, using 'track <number>' command."); + return (-1); + } + + if (str == NULL) { + g_critical("Usage: text <time-in-seconds> <event-type> <text-itself>"); + return (-2); + } + + /* Extract the time. Don't use strsep(3), it doesn't work on SunOS. */ + time = str; + str = strchr(str, ' '); + if (str != NULL) { + *str = '\0'; + str++; + } + + seconds = strtod(time, &end); + if (end - time != strlen(time)) { + g_critical("Time is supposed to be a number, without trailing characters."); + return (-3); + } + + /* Called with one parameter? */ + if (str == NULL) { + g_critical("Usage: text <time-in-seconds> <event-type> <text-itself>"); + return (-4); + } + + /* Extract the event type. */ + typestr = str; + str = strchr(str, ' '); + if (str != NULL) { + *str = '\0'; + str++; + } + + type = strtod(typestr, &end); + if (end - typestr != strlen(typestr)) { + g_critical("Type is supposed to be a number, without trailing characters."); + return (-4); + } + + if (type < 1 || type > 9) { + g_critical("Valid values for type are 1 - 9, inclusive."); + return (-5); + } + + /* Called with one parameter? */ + if (str == NULL) { + g_critical("Usage: text <time-in-seconds> <event-type> <text-itself>"); + return (-4); + } + + selected_event = smf_event_new_textual(type, str); + if (selected_event == NULL) { + g_critical("smf_event_new_textual() failed, event not created."); + return (-6); + } + + assert(smf_event_is_valid(selected_event)); + + 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, using 'track <number>' command."); + 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); + +static struct command_struct { + char *name; + int (*function)(char *command); + char *help; +} commands[] = {{"help", cmd_help, "Show this help."}, + {"?", cmd_help, NULL}, + {"load", cmd_load, "Load named file."}, + {"open", cmd_load}, + {"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."}, + {"add", cmd_eventadd, "Add an event and select it."}, + {"text", cmd_text, "Add textual event and select it."}, + {"eventadd", cmd_eventadd, NULL}, + {"eot", cmd_eventaddeot, "Add an End Of Track event."}, + {"eventaddeot", cmd_eventaddeot, NULL}, + {"eventrm", cmd_eventrm, NULL}, + {"rm", cmd_eventrm, "Remove currently selected event."}, + {"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) +{ + int i, padding_length; + char padding[COMMAND_LENGTH + 1]; + 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; + + padding_length = COMMAND_LENGTH - strlen(tmp->name); + assert(padding_length >= 0); + for (i = 0; i < padding_length; i++) + padding[i] = ' '; + padding[i] = '\0'; + + g_message("%s:%s%s", tmp->name, padding, 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 (strdup("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; + + command = line; + args = strchr(line, ' '); + if (args != NULL) { + *args = '\0'; + 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_line, *command, *next_command; + + command = command_line = read_command(); + + do { + next_command = strchr(command, ';'); + if (next_command != NULL) { + *next_command = '\0'; + next_command++; + } + + strip_unneeded_whitespace(command, 1024); + if (strlen(command) > 0) { + ret = execute_command(command); + if (ret) + g_warning("Command finished with error."); + } + + command = next_command; + + } while (command); + + free(command_line); +} + +#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 + +static void +usage(void) +{ + fprintf(stderr, "usage: smfsh [-V | file]\n"); + + exit(EX_USAGE); +} + +int +main(int argc, char *argv[]) +{ + int ch; + + while ((ch = getopt(argc, argv, "V")) != -1) { + switch (ch) { + case 'V': + cmd_version(NULL); + exit(EX_OK); + + case '?': + default: + usage(); + } + } + + 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) + cmd_load(argv[1]); + 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); +} + |