diff options
Diffstat (limited to 'src/share/grabbag')
-rw-r--r-- | src/share/grabbag/CMakeLists.txt | 14 | ||||
-rw-r--r-- | src/share/grabbag/alloc.c | 48 | ||||
-rw-r--r-- | src/share/grabbag/cuesheet.c | 681 | ||||
-rw-r--r-- | src/share/grabbag/file.c | 207 | ||||
-rw-r--r-- | src/share/grabbag/picture.c | 515 | ||||
-rw-r--r-- | src/share/grabbag/replaygain.c | 669 | ||||
-rw-r--r-- | src/share/grabbag/seektable.c | 105 | ||||
-rw-r--r-- | src/share/grabbag/snprintf.c | 101 |
8 files changed, 2340 insertions, 0 deletions
diff --git a/src/share/grabbag/CMakeLists.txt b/src/share/grabbag/CMakeLists.txt new file mode 100644 index 0000000..203ae3f --- /dev/null +++ b/src/share/grabbag/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(grabbag STATIC + alloc.c + cuesheet.c + file.c + picture.c + replaygain.c + seektable.c + snprintf.c) +target_link_libraries(grabbag PUBLIC + FLAC + replaygain_analysis) +if(TARGET win_utf8_io) + target_link_libraries(grabbag PUBLIC win_utf8_io) +endif() diff --git a/src/share/grabbag/alloc.c b/src/share/grabbag/alloc.c new file mode 100644 index 0000000..4e5fb60 --- /dev/null +++ b/src/share/grabbag/alloc.c @@ -0,0 +1,48 @@ +/* alloc - Convenience routines for safely allocating memory + * Copyright (C) 2007-2009 Josh Coalson + * Copyright (C) 2011-2023 Xiph.Org Foundation + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - 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. + * + * - Neither the name of the Xiph.org Foundation nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 FOUNDATION 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. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <stdlib.h> + +#include "share/alloc.h" + +void *safe_malloc_mul_2op_(size_t size1, size_t size2) +{ + if(!size1 || !size2) + return malloc(1); /* malloc(0) is undefined; FLAC src convention is to always allocate */ + if(size1 > SIZE_MAX / size2) + return 0; + return malloc(size1*size2); +} diff --git a/src/share/grabbag/cuesheet.c b/src/share/grabbag/cuesheet.c new file mode 100644 index 0000000..0d19ee7 --- /dev/null +++ b/src/share/grabbag/cuesheet.c @@ -0,0 +1,681 @@ +/* grabbag - Convenience lib for various routines common to several tools + * Copyright (C) 2002-2009 Josh Coalson + * Copyright (C) 2011-2023 Xiph.Org Foundation + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <limits.h> +#include "FLAC/assert.h" +#include "share/compat.h" +#include "share/grabbag.h" +#include "share/safe_str.h" + +uint32_t grabbag__cuesheet_msf_to_frame(uint32_t minutes, uint32_t seconds, uint32_t frames) +{ + return ((minutes * 60) + seconds) * 75 + frames; +} + +void grabbag__cuesheet_frame_to_msf(uint32_t frame, uint32_t *minutes, uint32_t *seconds, uint32_t *frames) +{ + *frames = frame % 75; + frame /= 75; + *seconds = frame % 60; + frame /= 60; + *minutes = frame; +} + +/* since we only care about values >= 0 or error, returns < 0 for any illegal string, else value */ +static FLAC__int64 local__parse_int64_(const char *s) +{ + FLAC__int64 ret = 0; + char c; + + if(*s == '\0') + return -1; + + while('\0' != (c = *s++)) + if(c >= '0' && c <= '9') { + if(ret >= (INT64_MAX / 10)) + return -1; + else + ret = ret * 10 + (c - '0'); + } + else + return -1; + + return ret; +} + +/* since we only care about values >= 0 or error, returns < 0 for any illegal string, else value */ +static int local__parse_int_(const char *s) +{ + FLAC__int64 ret64 = local__parse_int64_(s); + if(ret64 < 0 || ret64 > INT_MAX) + return -1; + return ret64; +} + +/* accept minute:second:frame syntax of '[0-9]+:[0-9][0-9]?:[0-9][0-9]?', but max second of 59 and max frame of 74, e.g. 0:0:0, 123:45:67 + * return sample number or <0 for error + * WATCHOUT: if sample rate is not evenly divisible by 75, the resulting sample number will be approximate + */ +static FLAC__int64 local__parse_msf_(const char *s, uint32_t sample_rate) +{ + FLAC__int64 ret, field; + char c; + + if(sample_rate == 0) + return -1; + + c = *s++; + if(c >= '0' && c <= '9') + field = (c - '0'); + else + return -1; + while(':' != (c = *s++)) { + if(c >= '0' && c <= '9') { + if(field >= (INT64_MAX / 10)) + return -1; + else + field = field * 10 + (c - '0'); + } + else + return -1; + } + + if(field >= INT64_MAX / (60 * sample_rate)) + return -1; + ret = field * 60 * sample_rate; + + c = *s++; + if(c >= '0' && c <= '9') + field = (c - '0'); + else + return -1; + if(':' != (c = *s++)) { + if(c >= '0' && c <= '9') { + field = field * 10 + (c - '0'); + c = *s++; + if(c != ':') + return -1; + } + else + return -1; + } + + if(field >= 60) + return -1; + + { + FLAC__int64 tmp = ret; + ret += field * sample_rate; + if(ret < tmp) + return -1; + } + + c = *s++; + if(c >= '0' && c <= '9') + field = (c - '0'); + else + return -1; + if('\0' != (c = *s++)) { + if(c >= '0' && c <= '9') { + field = field * 10 + (c - '0'); + c = *s++; + } + else + return -1; + } + + if(c != '\0') + return -1; + + if(field >= 75) + return -1; + + { + FLAC__int64 tmp = ret; + ret += field * (sample_rate / 75); + if(ret < tmp) + return -1; + } + + return ret; +} + +/* accept minute:second syntax of '[0-9]+:[0-9][0-9]?{,.[0-9]+}', but second < 60, e.g. 0:0.0, 3:5, 15:31.731 + * return sample number or <0 for error + * WATCHOUT: depending on the sample rate, the resulting sample number may be approximate with fractional seconds + */ +static FLAC__int64 local__parse_ms_(const char *s, uint32_t sample_rate) +{ + FLAC__int64 ret, field; + double x; + char c, *end; + + if(sample_rate == 0) + return -1; + + c = *s++; + if(c >= '0' && c <= '9') + field = (c - '0'); + else + return -1; + while(':' != (c = *s++)) { + if(c >= '0' && c <= '9') { + if(field >= (INT64_MAX / 10)) + return -1; + else + field = field * 10 + (c - '0'); + } + else + return -1; + } + + if(field >= INT64_MAX / (60 * sample_rate)) + return -1; + ret = field * 60 * sample_rate; + + if(strspn(s, "0123456789.") != strlen(s)) + return -1; + x = strtod(s, &end); + if(*end || end == s) + return -1; + if(x < 0.0 || x >= 60.0) + return -1; + + ret += (FLAC__int64)(x * sample_rate); + + return ret; +} + +static char *local__get_field_(char **s, FLAC__bool allow_quotes) +{ + FLAC__bool has_quote = false; + char *p; + + FLAC__ASSERT(0 != s); + + if(0 == *s) + return 0; + + /* skip leading whitespace */ + while(**s && 0 != strchr(" \t\r\n", **s)) + (*s)++; + + if(**s == 0) { + *s = 0; + return 0; + } + + if(allow_quotes && (**s == '"')) { + has_quote = true; + (*s)++; + if(**s == 0) { + *s = 0; + return 0; + } + } + + p = *s; + + if(has_quote) { + *s = strchr(*s, '\"'); + /* if there is no matching end quote, it's an error */ + if(0 == *s) + p = *s = 0; + else { + **s = '\0'; + (*s)++; + } + } + else { + while(**s && 0 == strchr(" \t\r\n", **s)) + (*s)++; + if(**s) { + **s = '\0'; + (*s)++; + } + else + *s = 0; + } + + return p; +} + +static FLAC__bool local__cuesheet_parse_(FILE *file, const char **error_message, uint32_t *last_line_read, FLAC__StreamMetadata *cuesheet, uint32_t sample_rate, FLAC__bool is_cdda, FLAC__uint64 lead_out_offset) +{ + char buffer[4096], *line, *field; + uint32_t forced_leadout_track_num = 0; + FLAC__uint64 forced_leadout_track_offset = 0; + int in_track_num = -1, in_index_num = -1; + FLAC__bool disc_has_catalog = false, track_has_flags = false, track_has_isrc = false, has_forced_leadout = false; + FLAC__StreamMetadata_CueSheet *cs = &cuesheet->data.cue_sheet; + + FLAC__ASSERT(!is_cdda || sample_rate == 44100); + /* double protection */ + if(is_cdda && sample_rate != 44100) { + *error_message = "CD-DA cuesheet only allowed with 44.1kHz sample rate"; + return false; + } + + cs->lead_in = is_cdda? 2 * 44100 /* The default lead-in size for CD-DA */ : 0; + cs->is_cd = is_cdda; + + while(0 != fgets(buffer, sizeof(buffer), file)) { + (*last_line_read)++; + line = buffer; + + { + size_t linelen = strlen(line); + if((linelen == sizeof(buffer)-1) && line[linelen-1] != '\n') { + *error_message = "line too long"; + return false; + } + } + + if(0 != (field = local__get_field_(&line, /*allow_quotes=*/false))) { + if(0 == FLAC__STRCASECMP(field, "CATALOG")) { + if(disc_has_catalog) { + *error_message = "found multiple CATALOG commands"; + return false; + } + if(0 == (field = local__get_field_(&line, /*allow_quotes=*/true))) { + *error_message = "CATALOG is missing catalog number"; + return false; + } + if(strlen(field) >= sizeof(cs->media_catalog_number)) { + *error_message = "CATALOG number is too long"; + return false; + } + if(is_cdda && (strlen(field) != 13 || strspn(field, "0123456789") != 13)) { + *error_message = "CD-DA CATALOG number must be 13 decimal digits"; + return false; + } + safe_strncpy(cs->media_catalog_number, field, sizeof(cs->media_catalog_number)); + disc_has_catalog = true; + } + else if(0 == FLAC__STRCASECMP(field, "FLAGS")) { + if(track_has_flags) { + *error_message = "found multiple FLAGS commands"; + return false; + } + if(in_track_num < 0 || in_index_num >= 0) { + *error_message = "FLAGS command must come after TRACK but before INDEX"; + return false; + } + while(0 != (field = local__get_field_(&line, /*allow_quotes=*/false))) { + if(0 == FLAC__STRCASECMP(field, "PRE")) + cs->tracks[cs->num_tracks-1].pre_emphasis = 1; + } + track_has_flags = true; + } + else if(0 == FLAC__STRCASECMP(field, "INDEX")) { + FLAC__int64 xx; + FLAC__StreamMetadata_CueSheet_Track *track = &cs->tracks[cs->num_tracks-1]; + if(in_track_num < 0) { + *error_message = "found INDEX before any TRACK"; + return false; + } + if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) { + *error_message = "INDEX is missing index number"; + return false; + } + in_index_num = local__parse_int_(field); + if(in_index_num < 0) { + *error_message = "INDEX has invalid index number"; + return false; + } + FLAC__ASSERT(cs->num_tracks > 0); + if(track->num_indices == 0) { + /* it's the first index point of the track */ + if(in_index_num > 1) { + *error_message = "first INDEX number of a TRACK must be 0 or 1"; + return false; + } + } + else { + if(in_index_num != track->indices[track->num_indices-1].number + 1) { + *error_message = "INDEX numbers must be sequential"; + return false; + } + } + if(is_cdda && in_index_num > 99) { + *error_message = "CD-DA INDEX number must be between 0 and 99, inclusive"; + return false; + } + /*@@@ search for duplicate track number? */ + if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) { + *error_message = "INDEX is missing an offset after the index number"; + return false; + } + /* first parse as minute:second:frame format */ + xx = local__parse_msf_(field, sample_rate); + if(xx < 0) { + /* CD-DA must use only MM:SS:FF format */ + if(is_cdda) { + *error_message = "illegal INDEX offset (not of the form MM:SS:FF)"; + return false; + } + /* as an extension for non-CD-DA we allow MM:SS.SS or raw sample number */ + xx = local__parse_ms_(field, sample_rate); + if(xx < 0) { + xx = local__parse_int64_(field); + if(xx < 0) { + *error_message = "illegal INDEX offset"; + return false; + } + } + } + else if(sample_rate % 75 && xx) { + /* only sample zero is exact */ + *error_message = "illegal INDEX offset (MM:SS:FF form not allowed if sample rate is not a multiple of 75)"; + return false; + } + if(is_cdda && cs->num_tracks == 1 && cs->tracks[0].num_indices == 0 && xx != 0) { + *error_message = "first INDEX of first TRACK must have an offset of 00:00:00"; + return false; + } + if(is_cdda && track->num_indices > 0 && (FLAC__uint64)xx <= track->indices[track->num_indices-1].offset) { + *error_message = "CD-DA INDEX offsets must increase in time"; + return false; + } + /* fill in track offset if it's the first index of the track */ + if(track->num_indices == 0) + track->offset = (FLAC__uint64)xx; + if(is_cdda && cs->num_tracks > 1) { + const FLAC__StreamMetadata_CueSheet_Track *prev = &cs->tracks[cs->num_tracks-2]; + if((FLAC__uint64)xx <= prev->offset + prev->indices[prev->num_indices-1].offset) { + *error_message = "CD-DA INDEX offsets must increase in time"; + return false; + } + } + if(!FLAC__metadata_object_cuesheet_track_insert_blank_index(cuesheet, cs->num_tracks-1, track->num_indices)) { + *error_message = "memory allocation error"; + return false; + } + track->indices[track->num_indices-1].offset = (FLAC__uint64)xx - track->offset; + track->indices[track->num_indices-1].number = in_index_num; + } + else if(0 == FLAC__STRCASECMP(field, "ISRC")) { + char *l, *r; + if(track_has_isrc) { + *error_message = "found multiple ISRC commands"; + return false; + } + if(in_track_num < 0 || in_index_num >= 0) { + *error_message = "ISRC command must come after TRACK but before INDEX"; + return false; + } + if(0 == (field = local__get_field_(&line, /*allow_quotes=*/true))) { + *error_message = "ISRC is missing ISRC number"; + return false; + } + /* strip out dashes */ + for(l = r = field; *r; r++) { + if(*r != '-') + *l++ = *r; + } + *l = '\0'; + if(strlen(field) != 12 || strspn(field, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") < 5 || strspn(field+5, "1234567890") != 7) { + *error_message = "invalid ISRC number"; + return false; + } + safe_strncpy(cs->tracks[cs->num_tracks-1].isrc, field, sizeof(cs->tracks[cs->num_tracks-1].isrc)); + track_has_isrc = true; + } + else if(0 == FLAC__STRCASECMP(field, "TRACK")) { + if(cs->num_tracks > 0) { + const FLAC__StreamMetadata_CueSheet_Track *prev = &cs->tracks[cs->num_tracks-1]; + if( + prev->num_indices == 0 || + ( + is_cdda && + ( + (prev->num_indices == 1 && prev->indices[0].number != 1) || + (prev->num_indices == 2 && prev->indices[0].number != 1 && prev->indices[1].number != 1) + ) + ) + ) { + *error_message = is_cdda? + "previous TRACK must specify at least one INDEX 01" : + "previous TRACK must specify at least one INDEX"; + return false; + } + } + if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) { + *error_message = "TRACK is missing track number"; + return false; + } + in_track_num = local__parse_int_(field); + if(in_track_num < 0) { + *error_message = "TRACK has invalid track number"; + return false; + } + if(in_track_num == 0) { + *error_message = "TRACK number must be greater than 0"; + return false; + } + if(is_cdda) { + if(in_track_num > 99) { + *error_message = "CD-DA TRACK number must be between 1 and 99, inclusive"; + return false; + } + } + else { + if(in_track_num == 255) { + *error_message = "TRACK number 255 is reserved for the lead-out"; + return false; + } + else if(in_track_num > 255) { + *error_message = "TRACK number must be between 1 and 254, inclusive"; + return false; + } + } + if(is_cdda && cs->num_tracks > 0 && in_track_num != cs->tracks[cs->num_tracks-1].number + 1) { + *error_message = "CD-DA TRACK numbers must be sequential"; + return false; + } + /*@@@ search for duplicate track number? */ + if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) { + *error_message = "TRACK is missing a track type after the track number"; + return false; + } + if(!FLAC__metadata_object_cuesheet_insert_blank_track(cuesheet, cs->num_tracks)) { + *error_message = "memory allocation error"; + return false; + } + cs->tracks[cs->num_tracks-1].number = in_track_num; + cs->tracks[cs->num_tracks-1].type = (0 == FLAC__STRCASECMP(field, "AUDIO"))? 0 : 1; /*@@@ should we be more strict with the value here? */ + in_index_num = -1; + track_has_flags = false; + track_has_isrc = false; + } + else if(0 == FLAC__STRCASECMP(field, "REM")) { + if(0 != (field = local__get_field_(&line, /*allow_quotes=*/false))) { + if(0 == strcmp(field, "FLAC__lead-in")) { + FLAC__int64 xx; + if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) { + *error_message = "FLAC__lead-in is missing offset"; + return false; + } + xx = local__parse_int64_(field); + if(xx < 0) { + *error_message = "illegal FLAC__lead-in offset"; + return false; + } + if(is_cdda && xx % 588 != 0) { + *error_message = "illegal CD-DA FLAC__lead-in offset, must be even multiple of 588 samples"; + return false; + } + cs->lead_in = (FLAC__uint64)xx; + } + else if(0 == strcmp(field, "FLAC__lead-out")) { + int track_num; + FLAC__int64 offset; + if(has_forced_leadout) { + *error_message = "multiple FLAC__lead-out commands"; + return false; + } + if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) { + *error_message = "FLAC__lead-out is missing track number"; + return false; + } + track_num = local__parse_int_(field); + if(track_num < 0) { + *error_message = "illegal FLAC__lead-out track number"; + return false; + } + forced_leadout_track_num = (uint32_t)track_num; + /*@@@ search for duplicate track number? */ + if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) { + *error_message = "FLAC__lead-out is missing offset"; + return false; + } + offset = local__parse_int64_(field); + if(offset < 0) { + *error_message = "illegal FLAC__lead-out offset"; + return false; + } + forced_leadout_track_offset = (FLAC__uint64)offset; + if(forced_leadout_track_offset != lead_out_offset) { + *error_message = "FLAC__lead-out offset does not match end-of-stream offset"; + return false; + } + has_forced_leadout = true; + } + } + } + } + } + + if(cs->num_tracks == 0) { + *error_message = "there must be at least one TRACK command"; + return false; + } + else { + const FLAC__StreamMetadata_CueSheet_Track *prev = &cs->tracks[cs->num_tracks-1]; + if( + prev->num_indices == 0 || + ( + is_cdda && + ( + (prev->num_indices == 1 && prev->indices[0].number != 1) || + (prev->num_indices == 2 && prev->indices[0].number != 1 && prev->indices[1].number != 1) + ) + ) + ) { + *error_message = is_cdda? + "previous TRACK must specify at least one INDEX 01" : + "previous TRACK must specify at least one INDEX"; + return false; + } + } + + if(!has_forced_leadout) { + forced_leadout_track_num = is_cdda? 170 : 255; + forced_leadout_track_offset = lead_out_offset; + } + if(!FLAC__metadata_object_cuesheet_insert_blank_track(cuesheet, cs->num_tracks)) { + *error_message = "memory allocation error"; + return false; + } + cs->tracks[cs->num_tracks-1].number = forced_leadout_track_num; + cs->tracks[cs->num_tracks-1].offset = forced_leadout_track_offset; + + if(!feof(file)) { + *error_message = "read error"; + return false; + } + return true; +} + +FLAC__StreamMetadata *grabbag__cuesheet_parse(FILE *file, const char **error_message, uint32_t *last_line_read, uint32_t sample_rate, FLAC__bool is_cdda, FLAC__uint64 lead_out_offset) +{ + FLAC__StreamMetadata *cuesheet; + + FLAC__ASSERT(0 != file); + FLAC__ASSERT(0 != error_message); + FLAC__ASSERT(0 != last_line_read); + + *last_line_read = 0; + cuesheet = FLAC__metadata_object_new(FLAC__METADATA_TYPE_CUESHEET); + + if(0 == cuesheet) { + *error_message = "memory allocation error"; + return 0; + } + + if(!local__cuesheet_parse_(file, error_message, last_line_read, cuesheet, sample_rate, is_cdda, lead_out_offset)) { + FLAC__metadata_object_delete(cuesheet); + return 0; + } + + return cuesheet; +} + +void grabbag__cuesheet_emit(FILE *file, const FLAC__StreamMetadata *cuesheet, const char *file_reference) +{ + const FLAC__StreamMetadata_CueSheet *cs; + uint32_t track_num, index_num; + + FLAC__ASSERT(0 != file); + FLAC__ASSERT(0 != cuesheet); + FLAC__ASSERT(cuesheet->type == FLAC__METADATA_TYPE_CUESHEET); + + cs = &cuesheet->data.cue_sheet; + + if(*(cs->media_catalog_number)) + fprintf(file, "CATALOG %s\n", cs->media_catalog_number); + fprintf(file, "FILE %s\n", file_reference); + + FLAC__ASSERT(cs->num_tracks > 0); + + for(track_num = 0; track_num < cs->num_tracks-1; track_num++) { + const FLAC__StreamMetadata_CueSheet_Track *track = cs->tracks + track_num; + + fprintf(file, " TRACK %02u %s\n", (uint32_t)track->number, track->type == 0? "AUDIO" : "DATA"); + + if(track->pre_emphasis) + fprintf(file, " FLAGS PRE\n"); + if(*(track->isrc)) + fprintf(file, " ISRC %s\n", track->isrc); + + for(index_num = 0; index_num < track->num_indices; index_num++) { + const FLAC__StreamMetadata_CueSheet_Index *indx = track->indices + index_num; + + fprintf(file, " INDEX %02u ", (uint32_t)indx->number); + if(cs->is_cd) { + const uint32_t logical_frame = (uint32_t)((track->offset + indx->offset) / (44100 / 75)); + uint32_t m, s, f; + grabbag__cuesheet_frame_to_msf(logical_frame, &m, &s, &f); + fprintf(file, "%02u:%02u:%02u\n", m, s, f); + } + else + fprintf(file, "%" PRIu64 "\n", (track->offset + indx->offset)); + } + } + + fprintf(file, "REM FLAC__lead-in %" PRIu64 "\n", cs->lead_in); + fprintf(file, "REM FLAC__lead-out %u %" PRIu64 "\n", (uint32_t)cs->tracks[track_num].number, cs->tracks[track_num].offset); +} diff --git a/src/share/grabbag/file.c b/src/share/grabbag/file.c new file mode 100644 index 0000000..307645f --- /dev/null +++ b/src/share/grabbag/file.c @@ -0,0 +1,207 @@ +/* grabbag - Convenience lib for various routines common to several tools + * Copyright (C) 2002-2009 Josh Coalson + * Copyright (C) 2011-2023 Xiph.Org Foundation + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#if defined _MSC_VER || defined __MINGW32__ +#include <sys/utime.h> /* for utime() */ +#include <io.h> /* for chmod(), _setmode(), unlink() */ +#include <fcntl.h> /* for _O_BINARY */ +#else +#include <sys/types.h> /* some flavors of BSD (like OS X) require this to get time_t */ +#endif +#if defined __EMX__ +#include <io.h> /* for setmode(), O_BINARY */ +#include <fcntl.h> /* for _O_BINARY */ +#endif +#include <sys/stat.h> /* for stat(), maybe chmod() */ +#if defined _WIN32 && !defined __CYGWIN__ +#else +#include <unistd.h> /* for unlink() */ +#endif +#include <stdio.h> +#include <stdlib.h> +#include <string.h> /* for strrchr() */ +#if defined _WIN32 && !defined __CYGWIN__ +// for GetFileInformationByHandle() etc +#include <windows.h> +#include <winbase.h> +#endif +#include "share/grabbag.h" +#include "share/compat.h" + + +void grabbag__file_copy_metadata(const char *srcpath, const char *destpath) +{ + struct flac_stat_s srcstat; + + if(0 == flac_stat(srcpath, &srcstat)) { +#if defined(_POSIX_C_SOURCE) && (_POSIX_C_SOURCE >= 200809L) && !defined(_WIN32) + struct timespec srctime[2] = {}; + srctime[0].tv_sec = srcstat.st_atime; + srctime[1].tv_sec = srcstat.st_mtime; +#else + struct utimbuf srctime; + srctime.actime = srcstat.st_atime; + srctime.modtime = srcstat.st_mtime; +#endif + (void)flac_chmod(destpath, srcstat.st_mode); + (void)flac_utime(destpath, &srctime); + } +} + +FLAC__off_t grabbag__file_get_filesize(const char *srcpath) +{ + struct flac_stat_s srcstat; + + if(0 == flac_stat(srcpath, &srcstat)) + return srcstat.st_size; + else + return -1; +} + +const char *grabbag__file_get_basename(const char *srcpath) +{ + const char *p; + + p = strrchr(srcpath, '/'); + if(0 == p) { +#if defined _WIN32 && !defined __CYGWIN__ + p = strrchr(srcpath, '\\'); + if(0 == p) +#endif + return srcpath; + } + return ++p; +} + +FLAC__bool grabbag__file_change_stats(const char *filename, FLAC__bool read_only) +{ + struct flac_stat_s stats; + + if(0 == flac_stat(filename, &stats)) { +#if !defined _MSC_VER && !defined __MINGW32__ + if(read_only) { + stats.st_mode &= ~S_IWUSR; + stats.st_mode &= ~S_IWGRP; + stats.st_mode &= ~S_IWOTH; + } + else { + stats.st_mode |= S_IWUSR; + } +#else + if(read_only) + stats.st_mode &= ~S_IWRITE; + else + stats.st_mode |= S_IWRITE; +#endif + if(0 != flac_chmod(filename, stats.st_mode)) + return false; + } + else + return false; + + return true; +} + +FLAC__bool grabbag__file_are_same(const char *f1, const char *f2) +{ +#if defined _WIN32 && !defined __CYGWIN__ +#if !defined(WINAPI_FAMILY_PARTITION) +#define WINAPI_FAMILY_PARTITION(x) x +#define WINAPI_PARTITION_DESKTOP 1 +#endif + /* see + * http://www.hydrogenaudio.org/forums/index.php?showtopic=49439&pid=444300&st=0 + * http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/getfileinformationbyhandle.asp + * http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/by_handle_file_information_str.asp + * http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/createfile.asp + * apparently both the files have to be open at the same time for the comparison to work + */ + FLAC__bool same = false; + BY_HANDLE_FILE_INFORMATION info1, info2; + HANDLE h1, h2; + BOOL ok = 1; + h1 = CreateFile_utf8(f1, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + h2 = CreateFile_utf8(f2, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if(h1 == INVALID_HANDLE_VALUE || h2 == INVALID_HANDLE_VALUE) + ok = 0; +#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) + ok &= GetFileInformationByHandle(h1, &info1); + ok &= GetFileInformationByHandle(h2, &info2); + if(ok) + same = + info1.dwVolumeSerialNumber == info2.dwVolumeSerialNumber && + info1.nFileIndexHigh == info2.nFileIndexHigh && + info1.nFileIndexLow == info2.nFileIndexLow + ; +#else // !WINAPI_PARTITION_DESKTOP + FILE_ID_INFO id_info1, id_info2; + same = GetFileInformationByHandleEx(h1, FileIdInfo, &id_info1, sizeof (id_info1)) && + GetFileInformationByHandleEx(h2, FileIdInfo, &id_info2, sizeof (id_info2)) && + id_info1.VolumeSerialNumber == id_info2.VolumeSerialNumber && + memcmp(&id_info1.FileId, &id_info2.FileId, sizeof(id_info1.FileId)) == 0; +#endif // !WINAPI_PARTITION_DESKTOP + if(h1 != INVALID_HANDLE_VALUE) + CloseHandle(h1); + if(h2 != INVALID_HANDLE_VALUE) + CloseHandle(h2); + return same; +#else + struct flac_stat_s s1, s2; + return f1 && f2 && flac_stat(f1, &s1) == 0 && flac_stat(f2, &s2) == 0 && s1.st_ino == s2.st_ino && s1.st_dev == s2.st_dev; +#endif +} + +FLAC__bool grabbag__file_remove_file(const char *filename) +{ + return grabbag__file_change_stats(filename, /*read_only=*/false) && 0 == flac_unlink(filename); +} + +FILE *grabbag__file_get_binary_stdin(void) +{ + /* if something breaks here it is probably due to the presence or + * absence of an underscore before the identifiers 'setmode', + * 'fileno', and/or 'O_BINARY'; check your system header files. + */ +#if defined _MSC_VER || defined __MINGW32__ + _setmode(_fileno(stdin), _O_BINARY); +#elif defined __EMX__ + setmode(fileno(stdin), O_BINARY); +#endif + + return stdin; +} + +FILE *grabbag__file_get_binary_stdout(void) +{ + /* if something breaks here it is probably due to the presence or + * absence of an underscore before the identifiers 'setmode', + * 'fileno', and/or 'O_BINARY'; check your system header files. + */ +#if defined _MSC_VER || defined __MINGW32__ + _setmode(_fileno(stdout), _O_BINARY); +#elif defined __EMX__ + setmode(fileno(stdout), O_BINARY); +#endif + + return stdout; +} diff --git a/src/share/grabbag/picture.c b/src/share/grabbag/picture.c new file mode 100644 index 0000000..9a4aafe --- /dev/null +++ b/src/share/grabbag/picture.c @@ -0,0 +1,515 @@ +/* grabbag - Convenience lib for various routines common to several tools + * Copyright (C) 2006-2009 Josh Coalson + * Copyright (C) 2011-2023 Xiph.Org Foundation + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include "share/alloc.h" +#include "share/grabbag.h" +#include "FLAC/assert.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include "share/compat.h" +#include "share/safe_str.h" + +/* slightly different that strndup(): this always copies 'size' bytes starting from s into a NUL-terminated string. */ +static char *local__strndup_(const char *s, size_t size) +{ + char *x = safe_malloc_add_2op_(size, /*+*/1); + if(x) { + memcpy(x, s, size); + x[size] = '\0'; + } + return x; +} + +static FLAC__bool local__parse_type_(const char *s, size_t len, FLAC__StreamMetadata_Picture *picture) +{ + size_t i; + FLAC__uint32 val = 0; + + picture->type = FLAC__STREAM_METADATA_PICTURE_TYPE_FRONT_COVER; + + if(len == 0) + return true; /* empty string implies default to 'front cover' */ + + for(i = 0; i < len; i++) { + if(s[i] >= '0' && s[i] <= '9') + val = 10*val + (FLAC__uint32)(s[i] - '0'); + else + return false; + } + + if(i == len) + picture->type = val; + else + return false; + + return true; +} + +static FLAC__bool local__parse_resolution_(const char *s, size_t len, FLAC__StreamMetadata_Picture *picture) +{ + int state = 0; + size_t i; + FLAC__uint32 val = 0; + + picture->width = picture->height = picture->depth = picture->colors = 0; + + if(len == 0) + return true; /* empty string implies client wants to get info from the file itself */ + + for(i = 0; i < len; i++) { + if(s[i] == 'x') { + if(state == 0) + picture->width = val; + else if(state == 1) + picture->height = val; + else + return false; + state++; + val = 0; + } + else if(s[i] == '/') { + if(state == 2) + picture->depth = val; + else + return false; + state++; + val = 0; + } + else if(s[i] >= '0' && s[i] <= '9') + val = 10*val + (FLAC__uint32)(s[i] - '0'); + else + return false; + } + + if(state < 2) + return false; + else if(state == 2) + picture->depth = val; + else if(state == 3) + picture->colors = val; + else + return false; + if(picture->depth < 32 && 1u<<picture->depth < picture->colors) + return false; + + return true; +} + +static FLAC__bool local__extract_mime_type_(FLAC__StreamMetadata *obj) +{ + if(obj->data.picture.data_length >= 8 && 0 == memcmp(obj->data.picture.data, "\x89PNG\x0d\x0a\x1a\x0a", 8)) + return FLAC__metadata_object_picture_set_mime_type(obj, "image/png", /*copy=*/true); + else if(obj->data.picture.data_length >= 6 && (0 == memcmp(obj->data.picture.data, "GIF87a", 6) || 0 == memcmp(obj->data.picture.data, "GIF89a", 6))) + return FLAC__metadata_object_picture_set_mime_type(obj, "image/gif", /*copy=*/true); + else if(obj->data.picture.data_length >= 2 && 0 == memcmp(obj->data.picture.data, "\xff\xd8", 2)) + return FLAC__metadata_object_picture_set_mime_type(obj, "image/jpeg", /*copy=*/true); + return false; +} + +static FLAC__bool local__extract_resolution_color_info_(FLAC__StreamMetadata_Picture *picture) +{ + const FLAC__byte *data = picture->data; + FLAC__uint32 len = picture->data_length; + + if(0 == strcmp(picture->mime_type, "image/png")) { + /* c.f. http://www.w3.org/TR/PNG/ */ + FLAC__bool need_palette = false; /* if IHDR has color_type=3, we need to also read the PLTE chunk to get the #colors */ + if(len < 8 || memcmp(data, "\x89PNG\x0d\x0a\x1a\x0a", 8)) + return false; + /* try to find IHDR chunk */ + data += 8; + len -= 8; + while(len > 12) { /* every PNG chunk must be at least 12 bytes long */ + const FLAC__uint32 clen = (FLAC__uint32)data[0] << 24 | (FLAC__uint32)data[1] << 16 | (FLAC__uint32)data[2] << 8 | (FLAC__uint32)data[3]; + /* First check whether clen makes sense or causes overflow in this bit of code */ + if(clen + 12 <= clen || clen + 12 > len) + return false; + else if(0 == memcmp(data+4, "IHDR", 4) && clen == 13) { + uint32_t color_type = data[17]; + picture->width = (FLAC__uint32)data[8] << 24 | (FLAC__uint32)data[9] << 16 | (FLAC__uint32)data[10] << 8 | (FLAC__uint32)data[11]; + picture->height = (FLAC__uint32)data[12] << 24 | (FLAC__uint32)data[13] << 16 | (FLAC__uint32)data[14] << 8 | (FLAC__uint32)data[15]; + if(color_type == 3) { + /* even though the bit depth for color_type==3 can be 1,2,4,or 8, + * the spec in 11.2.2 of http://www.w3.org/TR/PNG/ says that the + * sample depth is always 8 + */ + picture->depth = 8 * 3u; + need_palette = true; + data += 12 + clen; + len -= 12 + clen; + } + else { + if(color_type == 0) /* greyscale, 1 sample per pixel */ + picture->depth = (FLAC__uint32)data[16]; + if(color_type == 2) /* truecolor, 3 samples per pixel */ + picture->depth = (FLAC__uint32)data[16] * 3u; + if(color_type == 4) /* greyscale+alpha, 2 samples per pixel */ + picture->depth = (FLAC__uint32)data[16] * 2u; + if(color_type == 6) /* truecolor+alpha, 4 samples per pixel */ + picture->depth = (FLAC__uint32)data[16] * 4u; + picture->colors = 0; + return true; + } + } + else if(need_palette && 0 == memcmp(data+4, "PLTE", 4)) { + picture->colors = clen / 3u; + return true; + } + else { + data += 12 + clen; + len -= 12 + clen; + } + } + } + else if(0 == strcmp(picture->mime_type, "image/jpeg")) { + /* c.f. http://www.w3.org/Graphics/JPEG/itu-t81.pdf and Q22 of http://www.faqs.org/faqs/jpeg-faq/part1/ */ + if(len < 2 || memcmp(data, "\xff\xd8", 2)) + return false; + data += 2; + len -= 2; + while(1) { + /* look for sync FF byte */ + for( ; len > 0; data++, len--) { + if(*data == 0xff) + break; + } + if(len == 0) + return false; + /* eat any extra pad FF bytes before marker */ + for( ; len > 0; data++, len--) { + if(*data != 0xff) + break; + } + if(len == 0) + return false; + /* if we hit SOS or EOI, bail */ + if(*data == 0xda || *data == 0xd9) + return false; + /* looking for some SOFn */ + else if(memchr("\xc0\xc1\xc2\xc3\xc5\xc6\xc7\xc9\xca\xcb\xcd\xce\xcf", *data, 13)) { + data++; len--; /* skip marker byte */ + if(len < 2) + return false; + else { + const FLAC__uint32 clen = (FLAC__uint32)data[0] << 8 | (FLAC__uint32)data[1]; + if(clen < 8 || len < clen) + return false; + picture->width = (FLAC__uint32)data[5] << 8 | (FLAC__uint32)data[6]; + picture->height = (FLAC__uint32)data[3] << 8 | (FLAC__uint32)data[4]; + picture->depth = (FLAC__uint32)data[2] * (FLAC__uint32)data[7]; + picture->colors = 0; + return true; + } + } + /* else skip it */ + else { + data++; len--; /* skip marker byte */ + if(len < 2) + return false; + else { + const FLAC__uint32 clen = (FLAC__uint32)data[0] << 8 | (FLAC__uint32)data[1]; + if(clen < 2 || len < clen) + return false; + data += clen; + len -= clen; + } + } + } + } + else if(0 == strcmp(picture->mime_type, "image/gif")) { + /* c.f. http://www.w3.org/Graphics/GIF/spec-gif89a.txt */ + if(len < 14) + return false; + if(memcmp(data, "GIF87a", 6) && memcmp(data, "GIF89a", 6)) + return false; +#if 0 + /* according to the GIF spec, even if the GCTF is 0, the low 3 bits should still tell the total # colors used */ + if(data[10] & 0x80 == 0) + return false; +#endif + picture->width = (FLAC__uint32)data[6] | ((FLAC__uint32)data[7] << 8); + picture->height = (FLAC__uint32)data[8] | ((FLAC__uint32)data[9] << 8); +#if 0 + /* this value doesn't seem to be reliable... */ + picture->depth = (((FLAC__uint32)(data[10] & 0x70) >> 4) + 1) * 3u; +#else + /* ...just pessimistically assume it's 24-bit color without scanning all the color tables */ + picture->depth = 8u * 3u; +#endif + picture->colors = 1u << ((FLAC__uint32)(data[10] & 0x07) + 1u); + return true; + } + return false; +} + +static const char *error_messages[] = { + "memory allocation error", + "invalid picture specification", + "invalid picture specification: can't parse resolution/color part", + "unable to extract resolution and color info from URL, user must set explicitly", + "unable to extract resolution and color info from file, user must set explicitly", + "error opening picture file", + "error reading picture file", + "invalid picture type", + "unable to guess MIME type from file, user must set explicitly", + "type 1 icon must be a 32x32 pixel PNG", + "file not found", /* currently unused */ + "file is too large", + "empty file" +}; + +static const char * read_file (const char * filepath, FLAC__StreamMetadata * obj) +{ + const FLAC__off_t size = grabbag__file_get_filesize(filepath); + FLAC__byte *buffer; + FILE *file; + const char *error_message=NULL; + + if (size < 0) + return error_messages[5]; + + if (size == 0) + return error_messages[12]; + + if (size >= (FLAC__off_t)(1u << FLAC__STREAM_METADATA_LENGTH_LEN)) /* actual limit is less because of other fields in the PICTURE metadata block */ + return error_messages[11]; + + if ((buffer = safe_malloc_(size)) == NULL) + return error_messages[0]; + + if ((file = flac_fopen(filepath, "rb")) == NULL) { + free(buffer); + return error_messages[5]; + } + + if (fread(buffer, 1, size, file) != (size_t) size) { + fclose(file); + free(buffer); + return error_messages[6]; + } + fclose(file); + + if (!FLAC__metadata_object_picture_set_data(obj, buffer, (FLAC__uint32)size, /*copy=*/false)) + error_message = error_messages[6]; + /* try to extract MIME type if user left it blank */ + else if (*obj->data.picture.mime_type == '\0' && !local__extract_mime_type_(obj)) + error_message = error_messages[8]; + /* try to extract resolution/color info if user left it blank */ + else if ((obj->data.picture.width == 0 || obj->data.picture.height == 0 || obj->data.picture.depth == 0) && !local__extract_resolution_color_info_(&obj->data.picture)) + error_message = error_messages[4]; + /* check metadata block size */ + else if (obj->length >= (1u << FLAC__STREAM_METADATA_LENGTH_LEN)) + error_message = error_messages[11]; + + return error_message; +} + +FLAC__StreamMetadata *grabbag__picture_parse_specification(const char *spec, const char **error_message) +{ + FLAC__StreamMetadata *obj; + int state = 0; + + FLAC__ASSERT(0 != spec); + FLAC__ASSERT(0 != error_message); + + /* double protection */ + if(0 == spec) + return 0; + if(0 == error_message) + return 0; + + *error_message = 0; + + if(0 == (obj = FLAC__metadata_object_new(FLAC__METADATA_TYPE_PICTURE))) { + *error_message = error_messages[0]; + return obj; + } + + if(strchr(spec, '|')) { /* full format */ + const char *p; + char *q; + for(p = spec; *error_message==0 && *p; ) { + if(*p == '|') { + switch(state) { + case 0: /* type */ + if(!local__parse_type_(spec, p-spec, &obj->data.picture)) + *error_message = error_messages[7]; + break; + case 1: /* mime type */ + if(p-spec) { /* if blank, we'll try to guess later from the picture data */ + if(0 == (q = local__strndup_(spec, p-spec))) + *error_message = error_messages[0]; + else if(!FLAC__metadata_object_picture_set_mime_type(obj, q, /*copy=*/false)) + *error_message = error_messages[0]; + } + break; + case 2: /* description */ + if(0 == (q = local__strndup_(spec, p-spec))) + *error_message = error_messages[0]; + else if(!FLAC__metadata_object_picture_set_description(obj, (FLAC__byte*)q, /*copy=*/false)) + *error_message = error_messages[0]; + break; + case 3: /* resolution/color (e.g. [300x300x16[/1234]] */ + if(!local__parse_resolution_(spec, p-spec, &obj->data.picture)) + *error_message = error_messages[2]; + break; + default: + *error_message = error_messages[1]; + break; + } + p++; + spec = p; + state++; + } + else + p++; + } + } + else { /* simple format, filename only, everything else guessed */ + if(!local__parse_type_("", 0, &obj->data.picture)) /* use default picture type */ + *error_message = error_messages[7]; + /* leave MIME type to be filled in later */ + /* leave description empty */ + /* leave the rest to be filled in later: */ + else if(!local__parse_resolution_("", 0, &obj->data.picture)) + *error_message = error_messages[2]; + else + state = 4; + } + + /* parse filename, read file, try to extract resolution/color info if needed */ + if(*error_message == 0) { + if(state != 4) + *error_message = error_messages[1]; + else { /* 'spec' points to filename/URL */ + if(0 == strcmp(obj->data.picture.mime_type, "-->")) { /* magic MIME type means URL */ + if(strlen(spec) == 0) + *error_message = error_messages[1]; + else if(!FLAC__metadata_object_picture_set_data(obj, (FLAC__byte*)spec, strlen(spec), /*copy=*/true)) + *error_message = error_messages[0]; + else if(obj->data.picture.width == 0 || obj->data.picture.height == 0 || obj->data.picture.depth == 0) + *error_message = error_messages[3]; + } + else { /* regular picture file */ + *error_message = read_file (spec, obj); + } + } + } + + if(*error_message == 0) { + if( + obj->data.picture.type == FLAC__STREAM_METADATA_PICTURE_TYPE_FILE_ICON_STANDARD && + ( + (strcmp(obj->data.picture.mime_type, "image/png") && strcmp(obj->data.picture.mime_type, "-->")) || + obj->data.picture.width != 32 || + obj->data.picture.height != 32 + ) + ) + *error_message = error_messages[9]; + } + + if(*error_message && obj) { + FLAC__metadata_object_delete(obj); + obj = 0; + } + + return obj; +} + +FLAC__StreamMetadata *grabbag__picture_from_specification(int type, const char *mime_type_in, const char * description, + const PictureResolution * res, const char * filepath, const char **error_message) +{ + + FLAC__StreamMetadata *obj; + char mime_type [64] ; + + if (error_message == 0) + return 0; + + safe_strncpy(mime_type, mime_type_in, sizeof (mime_type)); + + *error_message = 0; + + if ((obj = FLAC__metadata_object_new(FLAC__METADATA_TYPE_PICTURE)) == 0) { + *error_message = error_messages[0]; + return obj; + } + + /* Picture type if known. */ + obj->data.picture.type = type >= 0 ? type : FLAC__STREAM_METADATA_PICTURE_TYPE_FRONT_COVER; + + /* Mime type if known. */ + if (mime_type_in && ! FLAC__metadata_object_picture_set_mime_type(obj, mime_type, /*copy=*/true)) { + *error_message = error_messages[0]; + return obj; + } + + /* Description if present. */ + if (description && ! FLAC__metadata_object_picture_set_description(obj, (FLAC__byte*) description, /*copy=*/true)) { + *error_message = error_messages[0]; + return obj; + } + + if (res == NULL) { + obj->data.picture.width = 0; + obj->data.picture.height = 0; + obj->data.picture.depth = 0; + obj->data.picture.colors = 0; + } + else { + obj->data.picture.width = res->width; + obj->data.picture.height = res->height; + obj->data.picture.depth = res->depth; + obj->data.picture.colors = res->colors; + } + + if (strcmp(obj->data.picture.mime_type, "-->") == 0) { /* magic MIME type means URL */ + if (!FLAC__metadata_object_picture_set_data(obj, (FLAC__byte*)filepath, strlen(filepath), /*copy=*/true)) + *error_message = error_messages[0]; + else if (obj->data.picture.width == 0 || obj->data.picture.height == 0 || obj->data.picture.depth == 0) + *error_message = error_messages[3]; + } + else { + *error_message = read_file (filepath, obj); + } + + if (*error_message == NULL) { + if ( + obj->data.picture.type == FLAC__STREAM_METADATA_PICTURE_TYPE_FILE_ICON_STANDARD && + ( + (strcmp(obj->data.picture.mime_type, "image/png") && strcmp(obj->data.picture.mime_type, "-->")) || + obj->data.picture.width != 32 || + obj->data.picture.height != 32 + ) + ) + *error_message = error_messages[9]; + } + + if (*error_message && obj) { + FLAC__metadata_object_delete(obj); + obj = 0; + } + + return obj; +} diff --git a/src/share/grabbag/replaygain.c b/src/share/grabbag/replaygain.c new file mode 100644 index 0000000..32c9603 --- /dev/null +++ b/src/share/grabbag/replaygain.c @@ -0,0 +1,669 @@ +/* grabbag - Convenience lib for various routines common to several tools + * Copyright (C) 2002-2009 Josh Coalson + * Copyright (C) 2011-2023 Xiph.Org Foundation + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <locale.h> +#include <math.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#if defined _MSC_VER || defined __MINGW32__ +#include <io.h> /* for chmod() */ +#endif +#include <sys/stat.h> /* for stat(), maybe chmod() */ + +#include "FLAC/assert.h" +#include "FLAC/metadata.h" +#include "FLAC/stream_decoder.h" +#include "share/grabbag.h" +#include "share/replaygain_analysis.h" +#include "share/safe_str.h" + +#ifdef local_min +#undef local_min +#endif +#define local_min(a,b) ((a)<(b)?(a):(b)) + +#ifdef local_max +#undef local_max +#endif +#define local_max(a,b) ((a)>(b)?(a):(b)) + +#ifdef abs32 +#undef abs32 +#endif +#define abs32(a) (((a)==INT32_MIN)?INT32_MAX:abs(a)) + +static const char *reference_format_ = "%s=%2.1f dB"; +static const char *gain_format_ = "%s=%+2.2f dB"; +static const char *peak_format_ = "%s=%1.8f"; + +static double album_peak_, title_peak_; + +const uint32_t GRABBAG__REPLAYGAIN_MAX_TAG_SPACE_REQUIRED = 190; +/* + FLAC__STREAM_METADATA_VORBIS_COMMENT_ENTRY_LENGTH_LEN/8 + 29 + 1 + 8 + + FLAC__STREAM_METADATA_VORBIS_COMMENT_ENTRY_LENGTH_LEN/8 + 21 + 1 + 10 + + FLAC__STREAM_METADATA_VORBIS_COMMENT_ENTRY_LENGTH_LEN/8 + 21 + 1 + 12 + + FLAC__STREAM_METADATA_VORBIS_COMMENT_ENTRY_LENGTH_LEN/8 + 21 + 1 + 10 + + FLAC__STREAM_METADATA_VORBIS_COMMENT_ENTRY_LENGTH_LEN/8 + 21 + 1 + 12 +*/ + +const FLAC__byte * const GRABBAG__REPLAYGAIN_TAG_REFERENCE_LOUDNESS = (const FLAC__byte * const)"REPLAYGAIN_REFERENCE_LOUDNESS"; +const FLAC__byte * const GRABBAG__REPLAYGAIN_TAG_TITLE_GAIN = (const FLAC__byte * const)"REPLAYGAIN_TRACK_GAIN"; +const FLAC__byte * const GRABBAG__REPLAYGAIN_TAG_TITLE_PEAK = (const FLAC__byte * const)"REPLAYGAIN_TRACK_PEAK"; +const FLAC__byte * const GRABBAG__REPLAYGAIN_TAG_ALBUM_GAIN = (const FLAC__byte * const)"REPLAYGAIN_ALBUM_GAIN"; +const FLAC__byte * const GRABBAG__REPLAYGAIN_TAG_ALBUM_PEAK = (const FLAC__byte * const)"REPLAYGAIN_ALBUM_PEAK"; + + +static FLAC__bool get_file_stats_(const char *filename, struct flac_stat_s *stats) +{ + FLAC__ASSERT(0 != filename); + FLAC__ASSERT(0 != stats); + return (0 == flac_stat(filename, stats)); +} + +static void set_file_stats_(const char *filename, struct flac_stat_s *stats) +{ + FLAC__ASSERT(0 != filename); + FLAC__ASSERT(0 != stats); + + (void)flac_chmod(filename, stats->st_mode); +} + +static FLAC__bool append_tag_(FLAC__StreamMetadata *block, const char *format, const FLAC__byte *name, float value) +{ + char buffer[256]; + char *saved_locale; + FLAC__StreamMetadata_VorbisComment_Entry entry; + + FLAC__ASSERT(0 != block); + FLAC__ASSERT(block->type == FLAC__METADATA_TYPE_VORBIS_COMMENT); + FLAC__ASSERT(0 != format); + FLAC__ASSERT(0 != name); + + buffer[sizeof(buffer)-1] = '\0'; + /* + * We need to save the old locale and switch to "C" because the locale + * influences the formatting of %f and we want it a certain way. + */ + saved_locale = strdup(setlocale(LC_ALL, 0)); + if (0 == saved_locale) + return false; + setlocale(LC_ALL, "C"); + flac_snprintf(buffer, sizeof(buffer), format, name, value); + setlocale(LC_ALL, saved_locale); + free(saved_locale); + + entry.entry = (FLAC__byte *)buffer; + entry.length = strlen(buffer); + + return FLAC__metadata_object_vorbiscomment_append_comment(block, entry, /*copy=*/true); +} + +FLAC__bool grabbag__replaygain_is_valid_sample_frequency(uint32_t sample_frequency) +{ + return ValidGainFrequency( sample_frequency ); +} + +FLAC__bool grabbag__replaygain_init(uint32_t sample_frequency) +{ + title_peak_ = album_peak_ = 0.0; + return InitGainAnalysis((long)sample_frequency) == INIT_GAIN_ANALYSIS_OK; +} + +FLAC__bool grabbag__replaygain_analyze(const FLAC__int32 * const input[], FLAC__bool is_stereo, uint32_t bps, uint32_t samples) +{ + /* using a small buffer improves data locality; we'd like it to fit easily in the dcache */ + static flac_float_t lbuffer[2048], rbuffer[2048]; + static const uint32_t nbuffer = sizeof(lbuffer) / sizeof(lbuffer[0]); + FLAC__int32 block_peak = 0, s; + uint32_t i, j; + + FLAC__ASSERT(bps >= FLAC__MIN_BITS_PER_SAMPLE && bps <= FLAC__MAX_BITS_PER_SAMPLE); + FLAC__ASSERT(FLAC__MIN_BITS_PER_SAMPLE == 4 && FLAC__MAX_BITS_PER_SAMPLE == 32); + + if(bps == 16) { + if(is_stereo) { + j = 0; + while(samples > 0) { + const uint32_t n = local_min(samples, nbuffer); + for(i = 0; i < n; i++, j++) { + s = input[0][j]; + lbuffer[i] = (flac_float_t)s; + s = abs(s); + block_peak = local_max(block_peak, s); + + s = input[1][j]; + rbuffer[i] = (flac_float_t)s; + s = abs(s); + block_peak = local_max(block_peak, s); + } + samples -= n; + if(AnalyzeSamples(lbuffer, rbuffer, n, 2) != GAIN_ANALYSIS_OK) + return false; + } + } + else { + j = 0; + while(samples > 0) { + const uint32_t n = local_min(samples, nbuffer); + for(i = 0; i < n; i++, j++) { + s = input[0][j]; + lbuffer[i] = (flac_float_t)s; + s = abs(s); + block_peak = local_max(block_peak, s); + } + samples -= n; + if(AnalyzeSamples(lbuffer, 0, n, 1) != GAIN_ANALYSIS_OK) + return false; + } + } + } + else { + const double scale = ( + (bps > 16)? + (double)1. / (double)(1u << (bps - 16)) : + (double)(1u << (16 - bps)) + ); + + if(is_stereo) { + j = 0; + while(samples > 0) { + const uint32_t n = local_min(samples, nbuffer); + for(i = 0; i < n; i++, j++) { + s = input[0][j]; + lbuffer[i] = (flac_float_t)(scale * (double)s); + s = abs32(s); + block_peak = local_max(block_peak, s); + + s = input[1][j]; + rbuffer[i] = (flac_float_t)(scale * (double)s); + s = abs32(s); + block_peak = local_max(block_peak, s); + } + samples -= n; + if(AnalyzeSamples(lbuffer, rbuffer, n, 2) != GAIN_ANALYSIS_OK) + return false; + } + } + else { + j = 0; + while(samples > 0) { + const uint32_t n = local_min(samples, nbuffer); + for(i = 0; i < n; i++, j++) { + s = input[0][j]; + lbuffer[i] = (flac_float_t)(scale * (double)s); + s = abs32(s); + block_peak = local_max(block_peak, s); + } + samples -= n; + if(AnalyzeSamples(lbuffer, 0, n, 1) != GAIN_ANALYSIS_OK) + return false; + } + } + } + + { + const double peak_scale = (double)(1u << (bps - 1)); + double peak = (double)block_peak / peak_scale; + if(peak > title_peak_) + title_peak_ = peak; + if(peak > album_peak_) + album_peak_ = peak; + } + + return true; +} + +void grabbag__replaygain_get_album(float *gain, float *peak) +{ + *gain = (float)GetAlbumGain(); + *peak = (float)album_peak_; + album_peak_ = 0.0; +} + +void grabbag__replaygain_get_title(float *gain, float *peak) +{ + *gain = (float)GetTitleGain(); + *peak = (float)title_peak_; + title_peak_ = 0.0; +} + + +typedef struct { + uint32_t channels; + uint32_t bits_per_sample; + uint32_t sample_rate; + FLAC__bool error; +} DecoderInstance; + +static FLAC__StreamDecoderWriteStatus write_callback_(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 * const buffer[], void *client_data) +{ + DecoderInstance *instance = (DecoderInstance*)client_data; + const uint32_t bits_per_sample = frame->header.bits_per_sample; + const uint32_t channels = frame->header.channels; + const uint32_t sample_rate = frame->header.sample_rate; + const uint32_t samples = frame->header.blocksize; + + (void)decoder; + + if( + !instance->error && + (channels == 2 || channels == 1) && + bits_per_sample == instance->bits_per_sample && + channels == instance->channels && + sample_rate == instance->sample_rate + ) { + instance->error = !grabbag__replaygain_analyze(buffer, channels==2, bits_per_sample, samples); + } + else { + instance->error = true; + } + + if(!instance->error) + return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; + else + return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; +} + +static void metadata_callback_(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata, void *client_data) +{ + DecoderInstance *instance = (DecoderInstance*)client_data; + + (void)decoder; + + if(metadata->type == FLAC__METADATA_TYPE_STREAMINFO) { + instance->bits_per_sample = metadata->data.stream_info.bits_per_sample; + instance->channels = metadata->data.stream_info.channels; + instance->sample_rate = metadata->data.stream_info.sample_rate; + + if(instance->channels != 1 && instance->channels != 2) { + instance->error = true; + return; + } + + if(!grabbag__replaygain_is_valid_sample_frequency(instance->sample_rate)) { + instance->error = true; + return; + } + } +} + +static void error_callback_(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data) +{ + DecoderInstance *instance = (DecoderInstance*)client_data; + + (void)decoder, (void)status; + + instance->error = true; +} + +const char *grabbag__replaygain_analyze_file(const char *filename, float *title_gain, float *title_peak) +{ + DecoderInstance instance; + FLAC__StreamDecoder *decoder = FLAC__stream_decoder_new(); + + if(0 == decoder) + return "memory allocation error"; + + instance.error = false; + + /* It does these three by default but lets be explicit: */ + FLAC__stream_decoder_set_md5_checking(decoder, false); + FLAC__stream_decoder_set_metadata_ignore_all(decoder); + FLAC__stream_decoder_set_metadata_respond(decoder, FLAC__METADATA_TYPE_STREAMINFO); + + if(FLAC__stream_decoder_init_file(decoder, filename, write_callback_, metadata_callback_, error_callback_, &instance) != FLAC__STREAM_DECODER_INIT_STATUS_OK) { + FLAC__stream_decoder_delete(decoder); + return "initializing decoder"; + } + + if(!FLAC__stream_decoder_process_until_end_of_stream(decoder) || instance.error) { + FLAC__stream_decoder_delete(decoder); + return "decoding file"; + } + + FLAC__stream_decoder_delete(decoder); + + grabbag__replaygain_get_title(title_gain, title_peak); + + return 0; +} + +const char *grabbag__replaygain_store_to_vorbiscomment(FLAC__StreamMetadata *block, float album_gain, float album_peak, float title_gain, float title_peak) +{ + const char *error; + + if(0 != (error = grabbag__replaygain_store_to_vorbiscomment_reference(block))) + return error; + + if(0 != (error = grabbag__replaygain_store_to_vorbiscomment_title(block, title_gain, title_peak))) + return error; + + if(0 != (error = grabbag__replaygain_store_to_vorbiscomment_album(block, album_gain, album_peak))) + return error; + + return 0; +} + +const char *grabbag__replaygain_store_to_vorbiscomment_reference(FLAC__StreamMetadata *block) +{ + FLAC__ASSERT(0 != block); + FLAC__ASSERT(block->type == FLAC__METADATA_TYPE_VORBIS_COMMENT); + + if(FLAC__metadata_object_vorbiscomment_remove_entries_matching(block, (const char *)GRABBAG__REPLAYGAIN_TAG_REFERENCE_LOUDNESS) < 0) + return "memory allocation error"; + + if(!append_tag_(block, reference_format_, GRABBAG__REPLAYGAIN_TAG_REFERENCE_LOUDNESS, ReplayGainReferenceLoudness)) + return "memory allocation error"; + + return 0; +} + +const char *grabbag__replaygain_store_to_vorbiscomment_album(FLAC__StreamMetadata *block, float album_gain, float album_peak) +{ + FLAC__ASSERT(0 != block); + FLAC__ASSERT(block->type == FLAC__METADATA_TYPE_VORBIS_COMMENT); + + if( + FLAC__metadata_object_vorbiscomment_remove_entries_matching(block, (const char *)GRABBAG__REPLAYGAIN_TAG_ALBUM_GAIN) < 0 || + FLAC__metadata_object_vorbiscomment_remove_entries_matching(block, (const char *)GRABBAG__REPLAYGAIN_TAG_ALBUM_PEAK) < 0 + ) + return "memory allocation error"; + + if( + !append_tag_(block, gain_format_, GRABBAG__REPLAYGAIN_TAG_ALBUM_GAIN, album_gain) || + !append_tag_(block, peak_format_, GRABBAG__REPLAYGAIN_TAG_ALBUM_PEAK, album_peak) + ) + return "memory allocation error"; + + return 0; +} + +const char *grabbag__replaygain_store_to_vorbiscomment_title(FLAC__StreamMetadata *block, float title_gain, float title_peak) +{ + FLAC__ASSERT(0 != block); + FLAC__ASSERT(block->type == FLAC__METADATA_TYPE_VORBIS_COMMENT); + + if( + FLAC__metadata_object_vorbiscomment_remove_entries_matching(block, (const char *)GRABBAG__REPLAYGAIN_TAG_TITLE_GAIN) < 0 || + FLAC__metadata_object_vorbiscomment_remove_entries_matching(block, (const char *)GRABBAG__REPLAYGAIN_TAG_TITLE_PEAK) < 0 + ) + return "memory allocation error"; + + if( + !append_tag_(block, gain_format_, GRABBAG__REPLAYGAIN_TAG_TITLE_GAIN, title_gain) || + !append_tag_(block, peak_format_, GRABBAG__REPLAYGAIN_TAG_TITLE_PEAK, title_peak) + ) + return "memory allocation error"; + + return 0; +} + +static const char *store_to_file_pre_(const char *filename, FLAC__Metadata_Chain **chain, FLAC__StreamMetadata **block) +{ + FLAC__Metadata_Iterator *iterator; + const char *error; + FLAC__bool found_vc_block = false; + + if(0 == (*chain = FLAC__metadata_chain_new())) + return "memory allocation error"; + + if(!FLAC__metadata_chain_read(*chain, filename)) { + error = FLAC__Metadata_ChainStatusString[FLAC__metadata_chain_status(*chain)]; + FLAC__metadata_chain_delete(*chain); + return error; + } + + if(0 == (iterator = FLAC__metadata_iterator_new())) { + FLAC__metadata_chain_delete(*chain); + return "memory allocation error"; + } + + FLAC__metadata_iterator_init(iterator, *chain); + + do { + *block = FLAC__metadata_iterator_get_block(iterator); + if((*block)->type == FLAC__METADATA_TYPE_VORBIS_COMMENT) + found_vc_block = true; + } while(!found_vc_block && FLAC__metadata_iterator_next(iterator)); + + if(!found_vc_block) { + /* create a new block */ + *block = FLAC__metadata_object_new(FLAC__METADATA_TYPE_VORBIS_COMMENT); + if(0 == *block) { + FLAC__metadata_chain_delete(*chain); + FLAC__metadata_iterator_delete(iterator); + return "memory allocation error"; + } + while(FLAC__metadata_iterator_next(iterator)) + ; + if(!FLAC__metadata_iterator_insert_block_after(iterator, *block)) { + error = FLAC__Metadata_ChainStatusString[FLAC__metadata_chain_status(*chain)]; + FLAC__metadata_chain_delete(*chain); + FLAC__metadata_iterator_delete(iterator); + return error; + } + /* iterator is left pointing to new block */ + FLAC__ASSERT(FLAC__metadata_iterator_get_block(iterator) == *block); + } + + FLAC__metadata_iterator_delete(iterator); + + FLAC__ASSERT(0 != *block); + FLAC__ASSERT((*block)->type == FLAC__METADATA_TYPE_VORBIS_COMMENT); + + return 0; +} + +static const char *store_to_file_post_(const char *filename, FLAC__Metadata_Chain *chain, FLAC__bool preserve_modtime) +{ + struct flac_stat_s stats; + const FLAC__bool have_stats = get_file_stats_(filename, &stats); + + (void)grabbag__file_change_stats(filename, /*read_only=*/false); + + FLAC__metadata_chain_sort_padding(chain); + if(!FLAC__metadata_chain_write(chain, /*use_padding=*/true, preserve_modtime)) { + const char *error; + error = FLAC__Metadata_ChainStatusString[FLAC__metadata_chain_status(chain)]; + FLAC__metadata_chain_delete(chain); + return error; + } + + FLAC__metadata_chain_delete(chain); + + if(have_stats) + set_file_stats_(filename, &stats); + + return 0; +} + +const char *grabbag__replaygain_store_to_file(const char *filename, float album_gain, float album_peak, float title_gain, float title_peak, FLAC__bool preserve_modtime) +{ + FLAC__Metadata_Chain *chain; + FLAC__StreamMetadata *block = NULL; + const char *error; + + if(0 != (error = store_to_file_pre_(filename, &chain, &block))) + return error; + + if(0 != (error = grabbag__replaygain_store_to_vorbiscomment(block, album_gain, album_peak, title_gain, title_peak))) { + FLAC__metadata_chain_delete(chain); + return error; + } + + if(0 != (error = store_to_file_post_(filename, chain, preserve_modtime))) + return error; + + return 0; +} + +const char *grabbag__replaygain_store_to_file_reference(const char *filename, FLAC__bool preserve_modtime) +{ + FLAC__Metadata_Chain *chain; + FLAC__StreamMetadata *block = NULL; + const char *error; + + if(0 != (error = store_to_file_pre_(filename, &chain, &block))) + return error; + + if(0 != (error = grabbag__replaygain_store_to_vorbiscomment_reference(block))) { + FLAC__metadata_chain_delete(chain); + return error; + } + + if(0 != (error = store_to_file_post_(filename, chain, preserve_modtime))) + return error; + + return 0; +} + +const char *grabbag__replaygain_store_to_file_album(const char *filename, float album_gain, float album_peak, FLAC__bool preserve_modtime) +{ + FLAC__Metadata_Chain *chain; + FLAC__StreamMetadata *block = NULL; + const char *error; + + if(0 != (error = store_to_file_pre_(filename, &chain, &block))) + return error; + + if(0 != (error = grabbag__replaygain_store_to_vorbiscomment_album(block, album_gain, album_peak))) { + FLAC__metadata_chain_delete(chain); + return error; + } + + if(0 != (error = store_to_file_post_(filename, chain, preserve_modtime))) + return error; + + return 0; +} + +const char *grabbag__replaygain_store_to_file_title(const char *filename, float title_gain, float title_peak, FLAC__bool preserve_modtime) +{ + FLAC__Metadata_Chain *chain; + FLAC__StreamMetadata *block = NULL; + const char *error; + + if(0 != (error = store_to_file_pre_(filename, &chain, &block))) + return error; + + if(0 != (error = grabbag__replaygain_store_to_vorbiscomment_title(block, title_gain, title_peak))) { + FLAC__metadata_chain_delete(chain); + return error; + } + + if(0 != (error = store_to_file_post_(filename, chain, preserve_modtime))) + return error; + + return 0; +} + +static FLAC__bool parse_double_(const FLAC__StreamMetadata_VorbisComment_Entry *entry, double *val) +{ + char s[32], *end; + const char *p, *q; + double v; + + FLAC__ASSERT(0 != entry); + FLAC__ASSERT(0 != val); + + p = (const char *)entry->entry; + q = strchr(p, '='); + if(0 == q) + return false; + q++; + safe_strncpy(s, q, local_min(sizeof(s), (size_t) (entry->length - (q-p)))); + + v = strtod(s, &end); + if(end == s) + return false; + + *val = v; + return true; +} + +FLAC__bool grabbag__replaygain_load_from_vorbiscomment(const FLAC__StreamMetadata *block, FLAC__bool album_mode, FLAC__bool strict, double *reference, double *gain, double *peak) +{ + int reference_offset, gain_offset, peak_offset; + char *saved_locale; + FLAC__bool res = true; + + FLAC__ASSERT(0 != block); + FLAC__ASSERT(0 != reference); + FLAC__ASSERT(0 != gain); + FLAC__ASSERT(0 != peak); + FLAC__ASSERT(block->type == FLAC__METADATA_TYPE_VORBIS_COMMENT); + + /* Default to current level until overridden by a detected tag; this + * will always be true until we change replaygain_analysis.c + */ + *reference = ReplayGainReferenceLoudness; + + /* + * We need to save the old locale and switch to "C" because the locale + * influences the behaviour of strtod and we want it a certain way. + */ + saved_locale = strdup(setlocale(LC_ALL, 0)); + if (0 == saved_locale) + return false; + setlocale(LC_ALL, "C"); + + if(0 <= (reference_offset = FLAC__metadata_object_vorbiscomment_find_entry_from(block, /*offset=*/0, (const char *)GRABBAG__REPLAYGAIN_TAG_REFERENCE_LOUDNESS))) + (void)parse_double_(block->data.vorbis_comment.comments + reference_offset, reference); + + if(0 > (gain_offset = FLAC__metadata_object_vorbiscomment_find_entry_from(block, /*offset=*/0, (const char *)(album_mode? GRABBAG__REPLAYGAIN_TAG_ALBUM_GAIN : GRABBAG__REPLAYGAIN_TAG_TITLE_GAIN)))) + res = false; + if(0 > (peak_offset = FLAC__metadata_object_vorbiscomment_find_entry_from(block, /*offset=*/0, (const char *)(album_mode? GRABBAG__REPLAYGAIN_TAG_ALBUM_PEAK : GRABBAG__REPLAYGAIN_TAG_TITLE_PEAK)))) + res = false; + + if(res && !parse_double_(block->data.vorbis_comment.comments + gain_offset, gain)) + res = false; + if(res && !parse_double_(block->data.vorbis_comment.comments + peak_offset, peak)) + res = false; + if(res && *peak < 0.0) + res = false; + + setlocale(LC_ALL, saved_locale); + free(saved_locale); + + /* something failed; retry with strict */ + if (!res && !strict) + res = grabbag__replaygain_load_from_vorbiscomment(block, !album_mode, /*strict=*/true, reference, gain, peak); + + return res; +} + +double grabbag__replaygain_compute_scale_factor(double peak, double gain, double preamp, FLAC__bool prevent_clipping) +{ + double scale; + FLAC__ASSERT(peak >= 0.0); + gain += preamp; + scale = (float) pow(10.0, gain * 0.05); + if(prevent_clipping && peak > 0.0) { + const double max_scale = (float)(1.0 / peak); + if(scale > max_scale) + scale = max_scale; + } + return scale; +} diff --git a/src/share/grabbag/seektable.c b/src/share/grabbag/seektable.c new file mode 100644 index 0000000..01caa99 --- /dev/null +++ b/src/share/grabbag/seektable.c @@ -0,0 +1,105 @@ +/* grabbag - Convenience lib for various routines common to several tools + * Copyright (C) 2002-2009 Josh Coalson + * Copyright (C) 2011-2023 Xiph.Org Foundation + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include "share/grabbag.h" +#include "share/compat.h" +#include "FLAC/assert.h" +#include <stdlib.h> /* for atoi() */ +#include <string.h> + +FLAC__bool grabbag__seektable_convert_specification_to_template(const char *spec, FLAC__bool only_explicit_placeholders, FLAC__uint64 total_samples_to_encode, uint32_t sample_rate, FLAC__StreamMetadata *seektable_template, FLAC__bool *spec_has_real_points) +{ + uint32_t i; + const char *pt; + + FLAC__ASSERT(0 != spec); + FLAC__ASSERT(0 != seektable_template); + FLAC__ASSERT(seektable_template->type == FLAC__METADATA_TYPE_SEEKTABLE); + + if(0 != spec_has_real_points) + *spec_has_real_points = false; + + for(pt = spec, i = 0; pt && *pt; i++) { + const char *q = strchr(pt, ';'); + FLAC__ASSERT(0 != q); + + if(q > pt) { + if(0 == strncmp(pt, "X;", 2)) { /* -S X */ + if(!FLAC__metadata_object_seektable_template_append_placeholders(seektable_template, 1)) + return false; + } + else if(q[-1] == 'x') { /* -S #x */ + if(total_samples_to_encode > 0) { /* we can only do these if we know the number of samples to encode up front */ + if(0 != spec_has_real_points) + *spec_has_real_points = true; + if(!only_explicit_placeholders) { + const int n = (uint32_t)atoi(pt); + if(n > 0) + if(!FLAC__metadata_object_seektable_template_append_spaced_points(seektable_template, (uint32_t)n, total_samples_to_encode)) + return false; + } + } + } + else if(q[-1] == 's') { /* -S #s */ + if(total_samples_to_encode > 0 && sample_rate > 0) { /* we can only do these if we know the number of samples and sample rate to encode up front */ + if(0 != spec_has_real_points) + *spec_has_real_points = true; + if(!only_explicit_placeholders) { + const double sec = atof(pt); + if(sec > 0.0) { + uint32_t samples = (uint32_t)(sec * (double)sample_rate); + /* Restrict seekpoints to two per second of audio. */ + samples = samples < sample_rate / 2 ? sample_rate / 2 : samples; + if(samples > 0) { + /* +1 for the initial point at sample 0 */ + if(!FLAC__metadata_object_seektable_template_append_spaced_points_by_samples(seektable_template, samples, total_samples_to_encode)) + return false; + } + } + } + } + } + else { /* -S # */ + if(0 != spec_has_real_points) + *spec_has_real_points = true; + if(!only_explicit_placeholders) { + char *endptr; + const FLAC__int64 n = (FLAC__int64)strtoll(pt, &endptr, 10); + if( + (n > 0 || (endptr > pt && *endptr == ';')) && /* is a valid number (extra check needed for "0") */ + (total_samples_to_encode == 0 || (FLAC__uint64)n < total_samples_to_encode) /* number is not >= the known total_samples_to_encode */ + ) + if(!FLAC__metadata_object_seektable_template_append_point(seektable_template, (FLAC__uint64)n)) + return false; + } + } + } + + pt = ++q; + } + + if(!FLAC__metadata_object_seektable_template_sort(seektable_template, /*compact=*/true)) + return false; + + return true; +} diff --git a/src/share/grabbag/snprintf.c b/src/share/grabbag/snprintf.c new file mode 100644 index 0000000..bd7ffba --- /dev/null +++ b/src/share/grabbag/snprintf.c @@ -0,0 +1,101 @@ +/* grabbag - Convenience lib for various routines common to several tools + * Copyright (C) 2013-2023 Xiph.Org Foundation + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - 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. + * + * - Neither the name of the Xiph.org Foundation nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 FOUNDATION 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. + */ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <stdio.h> +#include <stdarg.h> + +#include "share/compat.h" + +/* + * FLAC needs to compile and work correctly on systems with a normal ISO C99 + * snprintf as well as Microsoft Visual Studio which has an non-standards + * conformant snprint_s function. + * + * The important difference occurs when the resultant string (plus string + * terminator) would have been longer than the supplied size parameter. When + * this happens, ISO C's snprintf returns the length of resultant string, but + * does not over-write the end of the buffer. MS's snprintf_s in this case + * returns -1. + * + * The _MSC_VER code below attempts to modify the return code for vsnprintf_s + * to something that is more compatible with the behaviour of the ISO C version. + */ + +int +flac_snprintf(char *str, size_t size, const char *fmt, ...) +{ + va_list va; + int rc; + +#if defined _MSC_VER + if (size == 0) + return 1024; +#endif + + va_start (va, fmt); + +#if defined _MSC_VER + rc = vsnprintf_s (str, size, _TRUNCATE, fmt, va); + if (rc < 0) + rc = size - 1; +#elif defined __MINGW32__ + rc = __mingw_vsnprintf (str, size, fmt, va); +#else + rc = vsnprintf (str, size, fmt, va); +#endif + va_end (va); + + return rc; +} + +int +flac_vsnprintf(char *str, size_t size, const char *fmt, va_list va) +{ + int rc; + +#if defined _MSC_VER + if (size == 0) + return 1024; + rc = vsnprintf_s (str, size, _TRUNCATE, fmt, va); + if (rc < 0) + rc = size - 1; +#elif defined __MINGW32__ + rc = __mingw_vsnprintf (str, size, fmt, va); +#else + rc = vsnprintf (str, size, fmt, va); +#endif + + return rc; +} |