Coverage Report

Created: 2026-03-12 07:20

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mpv/demux/demux_cue.c
Line
Count
Source
1
/*
2
 * Original author: Uoti Urpala
3
 *
4
 * This file is part of mpv.
5
 *
6
 * mpv is free software; you can redistribute it and/or
7
 * modify it under the terms of the GNU Lesser General Public
8
 * License as published by the Free Software Foundation; either
9
 * version 2.1 of the License, or (at your option) any later version.
10
 *
11
 * mpv is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU Lesser General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU Lesser General Public
17
 * License along with mpv.  If not, see <http://www.gnu.org/licenses/>.
18
 */
19
20
#include <stdlib.h>
21
#include <stdbool.h>
22
#include <string.h>
23
#include <inttypes.h>
24
25
#include "osdep/io.h"
26
27
#include "mpv_talloc.h"
28
29
#include "misc/bstr.h"
30
#include "misc/charset_conv.h"
31
#include "common/msg.h"
32
#include "demux/demux.h"
33
#include "options/m_config.h"
34
#include "options/m_option.h"
35
#include "options/path.h"
36
#include "common/common.h"
37
#include "stream/stream.h"
38
#include "timeline.h"
39
40
#include "cue.h"
41
42
#define PROBE_SIZE 512
43
44
struct priv {
45
    struct cue_file *f;
46
};
47
48
static void add_source(struct timeline *tl, struct demuxer *d)
49
146
{
50
146
    MP_TARRAY_APPEND(tl, tl->sources, tl->num_sources, d);
51
146
}
52
53
static bool try_open(struct timeline *tl, char *filename)
54
0
{
55
0
    struct bstr bfilename = bstr0(filename);
56
    // Avoid trying to open itself or another .cue file. Best would be
57
    // to check the result of demuxer auto-detection, but the demuxer
58
    // API doesn't allow this without opening a full demuxer.
59
0
    if (bstr_case_endswith(bfilename, bstr0(".cue"))
60
0
        || bstrcasecmp(bstr0(tl->demuxer->filename), bfilename) == 0)
61
0
        return false;
62
63
0
    struct demuxer_params p = {
64
0
        .stream_flags = tl->stream_origin,
65
0
        .depth = tl->demuxer ? tl->demuxer->depth + 1 : 0,
66
0
    };
67
68
0
    struct demuxer *d = demux_open_url(filename, &p, tl->cancel, tl->global);
69
    // Since .bin files are raw PCM data with no headers, we have to explicitly
70
    // open them. Also, try to avoid to open files that are most likely not .bin
71
    // files, as that would only play noise. Checking the file extension is
72
    // fragile, but it's about the only way we have.
73
    // TODO: maybe also could check if the .bin file is a multiple of the Audio
74
    //       CD sector size (2352 bytes)
75
0
    if (!d && bstr_case_endswith(bfilename, bstr0(".bin"))) {
76
0
        MP_WARN(tl, "CUE: Opening as BIN file!\n");
77
0
        p.force_format = "rawaudio";
78
0
        d = demux_open_url(filename, &p, tl->cancel, tl->global);
79
0
    }
80
0
    if (d) {
81
0
        add_source(tl, d);
82
0
        return true;
83
0
    }
84
0
    MP_ERR(tl, "Could not open source '%s'!\n", filename);
85
0
    return false;
86
0
}
87
88
static bool open_source(struct timeline *tl, char *filename)
89
0
{
90
0
    void *ctx = talloc_new(NULL);
91
0
    bool res = false;
92
93
0
    struct bstr dirname = mp_dirname(tl->demuxer->filename);
94
95
0
    struct bstr base_filename = bstr0(mp_basename(filename));
96
0
    if (!base_filename.len) {
97
0
        MP_WARN(tl, "CUE: Invalid audio filename in .cue file!\n");
98
0
    } else {
99
0
        char *fullname = mp_path_join_bstr(ctx, dirname, base_filename);
100
0
        if (try_open(tl, fullname)) {
101
0
            res = true;
102
0
            goto out;
103
0
        }
104
0
    }
105
106
    // Try an audio file with the same name as the .cue file (but different
107
    // extension).
108
    // Rationale: this situation happens easily if the audio file or both files
109
    // are renamed.
110
111
0
    struct bstr cuefile =
112
0
        bstr_strip_ext(bstr0(mp_basename(tl->demuxer->filename)));
113
114
0
    DIR *d = opendir(bstrdup0(ctx, dirname));
115
0
    if (!d)
116
0
        goto out;
117
0
    struct dirent *de;
118
0
    while ((de = readdir(d))) {
119
0
        char *dename0 = de->d_name;
120
0
        struct bstr dename = bstr0(dename0);
121
0
        if (bstr_case_startswith(dename, cuefile)) {
122
0
            MP_WARN(tl, "CUE: No useful audio filename "
123
0
                    "in .cue file found, trying with '%s' instead!\n",
124
0
                    dename0);
125
0
            if (try_open(tl, mp_path_join_bstr(ctx, dirname, dename))) {
126
0
                res = true;
127
0
                break;
128
0
            }
129
0
        }
130
0
    }
131
0
    closedir(d);
132
133
0
out:
134
0
    talloc_free(ctx);
135
0
    if (!res)
136
0
        MP_ERR(tl, "CUE: Could not open audio file!\n");
137
0
    return res;
138
0
}
139
140
static void build_timeline(struct timeline *tl)
141
146
{
142
146
    struct priv *p = tl->demuxer->priv;
143
144
146
    void *ctx = talloc_new(NULL);
145
146
146
    add_source(tl, tl->demuxer);
147
148
146
    struct cue_track *tracks = NULL;
149
146
    size_t track_count = 0;
150
151
552
    for (size_t n = 0; n < p->f->num_tracks; n++) {
152
406
        struct cue_track *track = &p->f->tracks[n];
153
406
        if (track->filename) {
154
0
            MP_TARRAY_APPEND(ctx, tracks, track_count, *track);
155
406
        } else {
156
406
            MP_WARN(tl->demuxer, "No file specified for track entry %zd. "
157
406
                    "It will be removed\n", n + 1);
158
406
        }
159
406
    }
160
161
146
    if (track_count == 0) {
162
146
        MP_ERR(tl, "CUE: no tracks found!\n");
163
146
        goto out;
164
146
    }
165
166
    // Remove duplicate file entries. This might be too sophisticated, since
167
    // CUE files usually use either separate files for every single track, or
168
    // only one file for all tracks.
169
170
0
    char **files = 0;
171
0
    size_t file_count = 0;
172
173
0
    for (size_t n = 0; n < track_count; n++) {
174
0
        struct cue_track *track = &tracks[n];
175
0
        track->source = -1;
176
0
        for (size_t file = 0; file < file_count; file++) {
177
0
            if (strcmp(files[file], track->filename) == 0) {
178
0
                track->source = file;
179
0
                break;
180
0
            }
181
0
        }
182
0
        if (track->source == -1) {
183
0
            file_count++;
184
0
            files = talloc_realloc(ctx, files, char *, file_count);
185
0
            files[file_count - 1] = track->filename;
186
0
            track->source = file_count - 1;
187
0
        }
188
0
    }
189
190
0
    for (size_t i = 0; i < file_count; i++) {
191
0
        if (!open_source(tl, files[i]))
192
0
            goto out;
193
0
    }
194
195
0
    struct timeline_part *timeline = talloc_array_ptrtype(tl, timeline,
196
0
                                                          track_count + 1);
197
0
    struct demux_chapter *chapters = talloc_array_ptrtype(tl, chapters,
198
0
                                                          track_count);
199
0
    double starttime = 0;
200
0
    for (int i = 0; i < track_count; i++) {
201
0
        struct demuxer *source = tl->sources[1 + tracks[i].source];
202
0
        double duration;
203
0
        if (i + 1 < track_count && tracks[i].source == tracks[i + 1].source) {
204
0
            duration = tracks[i + 1].start - tracks[i].start;
205
0
        } else {
206
0
            duration = source->duration;
207
            // Two cases: 1) last track of a single-file cue, or 2) any track of
208
            // a multi-file cue. We need to do this for 1) only because the
209
            // timeline needs to be terminated with the length of the last
210
            // track.
211
0
            duration -= tracks[i].start;
212
0
        }
213
0
        if (duration < 0) {
214
0
            MP_WARN(tl, "CUE: Can't get duration of source file!\n");
215
            // xxx: do something more reasonable
216
0
            duration = 0.0;
217
0
        }
218
0
        timeline[i] = (struct timeline_part) {
219
0
            .start = starttime,
220
0
            .end = starttime + duration,
221
0
            .source_start = tracks[i].start,
222
0
            .source = source,
223
0
        };
224
0
        chapters[i] = (struct demux_chapter) {
225
0
            .pts = timeline[i].start,
226
0
            .metadata = mp_tags_dup(tl, tracks[i].tags),
227
0
        };
228
0
        starttime = timeline[i].end;
229
0
    }
230
231
0
    struct timeline_par *par = talloc_ptrtype(tl, par);
232
0
    *par = (struct timeline_par){
233
0
        .parts = timeline,
234
0
        .num_parts = track_count,
235
0
        .track_layout = timeline[0].source,
236
0
    };
237
238
0
    tl->chapters = chapters;
239
0
    tl->num_chapters = track_count;
240
0
    MP_TARRAY_APPEND(tl, tl->pars, tl->num_pars, par);
241
0
    tl->meta = par->track_layout;
242
0
    tl->format = "cue";
243
244
146
out:
245
146
    talloc_free(ctx);
246
146
}
247
248
static int try_open_file(struct demuxer *demuxer, enum demux_check check)
249
130k
{
250
130k
    if (!demuxer->access_references)
251
0
        return -1;
252
253
130k
    struct stream *s = demuxer->stream;
254
130k
    if (check >= DEMUX_CHECK_UNSAFE) {
255
130k
        char probe[PROBE_SIZE];
256
130k
        int len = stream_read_peek(s, probe, sizeof(probe));
257
130k
        if (len < 1 || !mp_probe_cue((bstr){probe, len}))
258
129k
            return -1;
259
130k
    }
260
747
    struct priv *p = talloc_zero(demuxer, struct priv);
261
747
    demuxer->priv = p;
262
747
    demuxer->fully_read = true;
263
747
    bstr data = stream_read_complete(s, p, 1000000);
264
747
    if (data.start == NULL)
265
0
        return -1;
266
267
747
    struct demux_opts *opts = mp_get_config_group(p, demuxer->global, &demux_conf);
268
747
    const char *charset = mp_charset_guess(p, demuxer->log, data, opts->meta_cp, 0);
269
747
    if (charset && !mp_charset_is_utf8(charset)) {
270
527
        MP_INFO(demuxer, "Using CUE charset: %s\n", charset);
271
527
        bstr utf8 = mp_iconv_to_utf8(demuxer->log, data, charset, MP_ICONV_VERBOSE);
272
527
        if (utf8.start && utf8.start != data.start) {
273
527
            ta_steal(data.start, utf8.start);
274
527
            data = utf8;
275
527
        }
276
527
    }
277
747
    talloc_free(opts);
278
279
747
    p->f = mp_parse_cue(data);
280
747
    talloc_steal(p, p->f);
281
747
    if (!p->f) {
282
601
        MP_ERR(demuxer, "error parsing input file!\n");
283
601
        return -1;
284
601
    }
285
286
146
    demux_close_stream(demuxer);
287
288
146
    mp_tags_merge(demuxer->metadata, p->f->tags);
289
146
    return 0;
290
747
}
291
292
const struct demuxer_desc demuxer_desc_cue = {
293
    .name = "cue",
294
    .desc = "CUE sheet",
295
    .open = try_open_file,
296
    .load_timeline = build_timeline,
297
};