aboutsummaryrefslogtreecommitdiffhomepage
path: root/demux/cue.c
blob: 216fe20c1a14c7ca8e86ffde46388cc55b27d927 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
/*
 * This file is part of mpv.
 *
 * mpv 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.
 *
 * mpv 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 mpv.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <inttypes.h>

#include "mpv_talloc.h"

#include "misc/bstr.h"
#include "common/common.h"
#include "common/tags.h"

#include "cue.h"

#define SECS_PER_CUE_FRAME (1.0/75.0)

enum cue_command {
    CUE_ERROR = -1,     // not a valid CUE command, or an unknown extension
    CUE_EMPTY,          // line with whitespace only
    CUE_UNUSED,         // valid CUE command, but ignored by this code
    CUE_FILE,
    CUE_TRACK,
    CUE_INDEX,
    CUE_TITLE,
    CUE_PERFORMER,
};

static const struct {
    enum cue_command command;
    const char *text;
} cue_command_strings[] = {
    { CUE_FILE, "FILE" },
    { CUE_TRACK, "TRACK" },
    { CUE_INDEX, "INDEX" },
    { CUE_TITLE, "TITLE" },
    { CUE_UNUSED, "CATALOG" },
    { CUE_UNUSED, "CDTEXTFILE" },
    { CUE_UNUSED, "FLAGS" },
    { CUE_UNUSED, "ISRC" },
    { CUE_PERFORMER, "PERFORMER" },
    { CUE_UNUSED, "POSTGAP" },
    { CUE_UNUSED, "PREGAP" },
    { CUE_UNUSED, "REM" },
    { CUE_UNUSED, "SONGWRITER" },
    { CUE_UNUSED, "MESSAGE" },
    { -1 },
};

static enum cue_command read_cmd(struct bstr *data, struct bstr *out_params)
{
    struct bstr line = bstr_strip_linebreaks(bstr_getline(*data, data));
    line = bstr_lstrip(line);
    if (line.len == 0)
        return CUE_EMPTY;
    for (int n = 0; cue_command_strings[n].command != -1; n++) {
        struct bstr name = bstr0(cue_command_strings[n].text);
        if (bstr_case_startswith(line, name)) {
            struct bstr rest = bstr_cut(line, name.len);
            if (rest.len && !strchr(WHITESPACE, rest.start[0]))
                continue;
            if (out_params)
                *out_params = bstr_lstrip(rest);
            return cue_command_strings[n].command;
        }
    }
    return CUE_ERROR;
}

static bool eat_char(struct bstr *data, char ch)
{
    if (data->len && data->start[0] == ch) {
        *data = bstr_cut(*data, 1);
        return true;
    } else {
        return false;
    }
}

static char *read_quoted(void *talloc_ctx, struct bstr *data)
{
    *data = bstr_lstrip(*data);
    if (!eat_char(data, '"'))
        return NULL;
    int end = bstrchr(*data, '"');
    if (end < 0)
        return NULL;
    struct bstr res = bstr_splice(*data, 0, end);
    *data = bstr_cut(*data, end + 1);
    return bstrto0(talloc_ctx, res);
}

static struct bstr strip_quotes(struct bstr data)
{
    bstr s = data;
    if (bstr_eatstart0(&s, "\"") && bstr_eatend0(&s, "\""))
        return s;
    return data;
}

// Read a 2 digit unsigned decimal integer.
// Return -1 on failure.
static int read_int_2(struct bstr *data)
{
    *data = bstr_lstrip(*data);
    if (data->len && data->start[0] == '-')
        return -1;
    struct bstr s = *data;
    int res = (int)bstrtoll(s, &s, 10);
    if (data->len == s.len || data->len - s.len > 2)
        return -1;
    *data = s;
    return res;
}

static double read_time(struct bstr *data)
{
    struct bstr s = *data;
    bool ok = true;
    double t1 = read_int_2(&s);
    ok = eat_char(&s, ':') && ok;
    double t2 = read_int_2(&s);
    ok = eat_char(&s, ':') && ok;
    double t3 = read_int_2(&s);
    ok = ok && t1 >= 0 && t2 >= 0 && t3 >= 0;
    return ok ? t1 * 60.0 + t2 + t3 * SECS_PER_CUE_FRAME : 0;
}

static struct bstr skip_utf8_bom(struct bstr data)
{
    return bstr_startswith0(data, "\xEF\xBB\xBF") ? bstr_cut(data, 3) : data;
}

// Check if the text in data is most likely CUE data. This is used by the
// demuxer code to check the file type.
// data is the start of the probed file, possibly cut off at a random point.
bool mp_probe_cue(struct bstr data)
{
    bool valid = false;
    data = skip_utf8_bom(data);
    for (;;) {
        enum cue_command cmd = read_cmd(&data, NULL);
        // End reached. Since the line was most likely cut off, don't use the
        // result of the last parsing call.
        if (data.len == 0)
            break;
        if (cmd == CUE_ERROR)
            return false;
        if (cmd != CUE_EMPTY)
            valid = true;
    }
    return valid;
}

struct cue_file *mp_parse_cue(struct bstr data)
{
    struct cue_file *f = talloc_zero(NULL, struct cue_file);
    f->tags = talloc_zero(f, struct mp_tags);

    data = skip_utf8_bom(data);

    char *filename = NULL;
    // Global metadata, and copied into new tracks.
    struct cue_track proto_track = {0};
    struct cue_track *cur_track = NULL;

    while (data.len) {
        struct bstr param;
        int cmd = read_cmd(&data, &param);
        switch (cmd) {
        case CUE_ERROR:
            talloc_free(f);
            return NULL;
        case CUE_TRACK: {
            MP_TARRAY_GROW(f, f->tracks, f->num_tracks);
            f->num_tracks += 1;
            cur_track = &f->tracks[f->num_tracks - 1];
            *cur_track = proto_track;
            cur_track->tags = talloc_zero(f, struct mp_tags);
            break;
        }
        case CUE_TITLE:
        case CUE_PERFORMER: {
            static const char *metanames[] = {
                [CUE_TITLE] = "title",
                [CUE_PERFORMER] = "performer",
            };
            struct mp_tags *tags = cur_track ? cur_track->tags : f->tags;
            mp_tags_set_bstr(tags, bstr0(metanames[cmd]), strip_quotes(param));
            break;
        }
        case CUE_INDEX: {
            int type = read_int_2(&param);
            double time = read_time(&param);
            if (cur_track) {
                if (type == 1) {
                    cur_track->start = time;
                    cur_track->filename = filename;
                } else if (type == 0) {
                    cur_track->pregap_start = time;
                }
            }
            break;
        }
        case CUE_FILE:
            // NOTE: FILE comes before TRACK, so don't use cur_track->filename
            filename = read_quoted(f, &param);
            break;
        }
    }

    return f;
}

int mp_check_embedded_cue(struct cue_file *f)
{
    char *fn0 = f->tracks[0].filename;
    for (int n = 1; n < f->num_tracks; n++) {
        char *fn = f->tracks[n].filename;
        // both filenames have the same address (including NULL)
        if (fn0 == fn)
            continue;
        // only one filename is NULL, or the strings don't match
        if (!fn0 || !fn || strcmp(fn0, fn) != 0)
            return -1;
    }
    return 0;
}