mirror of
https://github.com/Ardour/ardour.git
synced 2025-12-07 23:35:03 +01:00
This would realloc a buffer for every event, making it absurdly slow for large MIDI files (say, 10k events). Use the somewhat standard heuristic of doubling the requested allocation every time we need to increase the size. This results in a speedup of 40-100x when saving SMF to disk
695 lines
16 KiB
C
695 lines
16 KiB
C
/*-
|
|
* 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>
|
|
|
|
#include "pbd/assert.h"
|
|
|
|
#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;
|
|
|
|
if (smf->file_buffer_capacity >= smf->file_buffer_length + length) {
|
|
smf->file_buffer_length += length;
|
|
} else {
|
|
|
|
if (smf->file_buffer_capacity == 0) {
|
|
smf->file_buffer_capacity = length * 2;
|
|
} else {
|
|
smf->file_buffer_capacity *= 2;
|
|
}
|
|
smf->file_buffer = realloc(smf->file_buffer, smf->file_buffer_capacity);
|
|
if (smf->file_buffer == NULL) {
|
|
g_warning("realloc(3) failed: %s", strerror(errno));
|
|
smf->file_buffer_length = 0;
|
|
smf->file_buffer_capacity = 0;
|
|
return (NULL);
|
|
}
|
|
|
|
smf->file_buffer_length += length;
|
|
|
|
/* 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 = GUINT32_TO_BE(6);
|
|
mthd_chunk.format = GUINT16_TO_BE(smf->format);
|
|
mthd_chunk.number_of_tracks = GUINT16_TO_BE(smf->number_of_tracks);
|
|
mthd_chunk.division = GUINT16_TO_BE(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;
|
|
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);
|
|
int copied_length;
|
|
copied_length = snprintf((char *)event->midi_buffer + vlq_length + 2, event->midi_buffer_length - vlq_length - 2, "%s", text);
|
|
x_assert (copied_length, copied_length == text_length);
|
|
|
|
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 = GUINT32_TO_BE(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;
|
|
smf->file_buffer_capacity = 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(labs((long)(a->time_pulses - b->time_pulses)) <= 2);
|
|
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);
|
|
}
|