The updated patch along with sample LRC file.

Moved to SCNu64 during reading timestamp as Derek suggests.

I have no idea on how to use FATE. Please help me, if possible, to add
this lrc file to the FATE dataset. Thanks.

Wish to see it in FFmpeg 2.4 release.
>From 5ba3c51eaa8a46f813acdea344e987cb73993652 Mon Sep 17 00:00:00 2001
From: Star Brilliant <m13...@hotmail.com>
Date: Wed, 9 Jul 2014 14:24:05 +0800
Subject: [PATCH] AVFormat: LRC demuxer and muxer

---
 Changelog                |   1 +
 doc/general.texi         |   1 +
 libavformat/Makefile     |   2 +
 libavformat/allformats.c |   1 +
 libavformat/lrc.c        |  34 +++++++
 libavformat/lrc.h        |  29 ++++++
 libavformat/lrcdec.c     | 248 +++++++++++++++++++++++++++++++++++++++++++++++
 libavformat/lrcenc.c     | 151 +++++++++++++++++++++++++++++
 8 files changed, 467 insertions(+)
 create mode 100644 libavformat/lrc.c
 create mode 100644 libavformat/lrc.h
 create mode 100644 libavformat/lrcdec.c
 create mode 100644 libavformat/lrcenc.c

diff --git a/Changelog b/Changelog
index fa443dc..75bde5b 100644
--- a/Changelog
+++ b/Changelog
@@ -29,6 +29,7 @@ version <next>:
 - showcqt multimedia filter
 - zoompan filter
 - signalstats filter
+- LRC demuxer and muxer
 
 
 version 2.2:
diff --git a/doc/general.texi b/doc/general.texi
index 86569a2..c008261 100644
--- a/doc/general.texi
+++ b/doc/general.texi
@@ -310,6 +310,7 @@ library:
     @tab Used by Linux Media Labs MPEG-4 PCI boards
 @item LOAS                      @tab   @tab X
     @tab contains LATM multiplexed AAC audio
+@item LRC                       @tab X @tab X
 @item LVF                       @tab   @tab X
 @item LXF                       @tab   @tab X
     @tab VR native stream format, used by Leitch/Harris' video servers.
diff --git a/libavformat/Makefile b/libavformat/Makefile
index ecae4d0..3218c4c 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -202,6 +202,8 @@ OBJS-$(CONFIG_LATM_DEMUXER)              += rawdec.o
 OBJS-$(CONFIG_LATM_MUXER)                += latmenc.o rawenc.o
 OBJS-$(CONFIG_LMLM4_DEMUXER)             += lmlm4.o
 OBJS-$(CONFIG_LOAS_DEMUXER)              += loasdec.o rawdec.o
+OBJS-$(CONFIG_LRC_DEMUXER)               += lrcdec.o lrc.o subtitles.o
+OBJS-$(CONFIG_LRC_MUXER)                 += lrcenc.o lrc.o
 OBJS-$(CONFIG_LVF_DEMUXER)               += lvfdec.o
 OBJS-$(CONFIG_LXF_DEMUXER)               += lxfdec.o
 OBJS-$(CONFIG_M4V_DEMUXER)               += m4vdec.o rawdec.o
diff --git a/libavformat/allformats.c b/libavformat/allformats.c
index dc5557c..fff05ee 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -163,6 +163,7 @@ void av_register_all(void)
     REGISTER_MUXDEMUX(LATM,             latm);
     REGISTER_DEMUXER (LMLM4,            lmlm4);
     REGISTER_DEMUXER (LOAS,             loas);
+    REGISTER_MUXDEMUX(LRC,              lrc);
     REGISTER_DEMUXER (LVF,              lvf);
     REGISTER_DEMUXER (LXF,              lxf);
     REGISTER_MUXDEMUX(M4V,              m4v);
diff --git a/libavformat/lrc.c b/libavformat/lrc.c
new file mode 100644
index 0000000..139c650
--- /dev/null
+++ b/libavformat/lrc.c
@@ -0,0 +1,34 @@
+/*
+ * LRC lyrics file format decoder
+ * Copyright (c) 2014 StarBrilliant <m13...@hotmail.com>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg 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.
+ *
+ * FFmpeg 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 FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "metadata.h"
+#include "lrc.h"
+
+const AVMetadataConv ff_lrc_metadata_conv[] = {
+    {"ti", "title"},
+    {"al", "album"},
+    {"ar", "artist"},
+    {"au", "author"},
+    {"by", "creator"},
+    {"re", "encoder"},
+    {"ve", "encoder_version"},
+    {0, 0}
+};
diff --git a/libavformat/lrc.h b/libavformat/lrc.h
new file mode 100644
index 0000000..0297bc1
--- /dev/null
+++ b/libavformat/lrc.h
@@ -0,0 +1,29 @@
+/*
+ * LRC lyrics file format decoder
+ * Copyright (c) 2014 StarBrilliant <m13...@hotmail.com>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg 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.
+ *
+ * FFmpeg 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 FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVFORMAT_LRC_H
+#define AVFORMAT_LRC_H
+
+#include "metadata.h"
+
+extern const AVMetadataConv ff_lrc_metadata_conv[];
+
+#endif
diff --git a/libavformat/lrcdec.c b/libavformat/lrcdec.c
new file mode 100644
index 0000000..3b02e34
--- /dev/null
+++ b/libavformat/lrcdec.c
@@ -0,0 +1,248 @@
+/*
+ * LRC lyrics file format decoder
+ * Copyright (c) 2014 StarBrilliant <m13...@hotmail.com>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg 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.
+ *
+ * FFmpeg 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 FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <inttypes.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "avformat.h"
+#include "internal.h"
+#include "lrc.h"
+#include "metadata.h"
+#include "subtitles.h"
+#include "libavutil/bprint.h"
+#include "libavutil/dict.h"
+
+typedef struct LRCContext {
+    FFDemuxSubtitlesQueue q;
+    int64_t ts_offset; // offset metadata item
+} LRCContext;
+
+static int64_t find_header(const char *p)
+{
+    int64_t offset = 0;
+    while(p[offset] == ' ' || p[offset] == '\t') {
+        offset++;
+    }
+    if(p[offset] == '[' && p[offset + 1] >= 'a' && p[offset + 1] <= 'z') {
+        return offset;
+    } else {
+        return -1;
+    }
+}
+
+static int64_t count_ts(const char *p)
+{
+    int64_t offset = 0;
+    int in_brackets = 0;
+
+    for(;;) {
+        if(p[offset] == ' ' || p[offset] == '\t') {
+            offset++;
+        } else if(p[offset] == '[') {
+            offset++;
+            in_brackets++;
+        } else if (p[offset] == ']' && in_brackets) {
+            offset++;
+            in_brackets--;
+        } else if(in_brackets &&
+                 (p[offset] == ':' || p[offset] == '.' || p[offset] == '-' ||
+                 (p[offset] >= '0' && p[offset] <= '9'))) {
+            offset++;
+        } else {
+            break;
+        }
+    }
+    return offset;
+}
+
+static int64_t read_ts(const char *p, int64_t *start)
+{
+    int64_t offset = 0;
+    uint64_t mm, ss, cs;
+
+    while(p[offset] == ' ' || p[offset] == '\t') {
+        offset++;
+    }
+    if(p[offset] != '[') {
+        return 0;
+    }
+    if(sscanf(p, "[-%"SCNu64":%"SCNu64".%"SCNu64"]", &mm, &ss, &cs) == 3) {
+        /* Just in case negative pts, players may drop it but we won't. */
+        *start = -(int64_t) (mm*60000 + ss*1000 + cs*10);
+    } else if(sscanf(p, "[%"SCNu64":%"SCNu64".%"SCNu64"]", &mm, &ss, &cs) == 3) {
+        *start = mm*60000 + ss*1000 + cs*10;
+    } else {
+        return 0;
+    }
+    do {
+        offset++;
+    } while(p[offset] && p[offset-1] != ']');
+    return offset;
+}
+
+static int64_t read_line(AVBPrint *buf, AVIOContext *pb)
+{
+    int64_t pos = avio_tell(pb);
+
+    av_bprint_clear(buf);
+    while(!url_feof(pb)) {
+        int c = avio_r8(pb);
+        if(c != '\r') {
+            av_bprint_chars(buf, c, 1);
+        }
+        if(c == '\n') {
+            break;
+        }
+    }
+    return pos;
+}
+
+static int lrc_probe(AVProbeData *p)
+{
+    int64_t offset = 0;
+    int64_t mm;
+    uint64_t ss, cs;
+    const AVMetadataConv *metadata_item;
+
+    if(!memcmp(p->buf, "\xef\xbb\xbf", 3)) { // Skip UTF-8 BOM header
+        offset += 3;
+    }
+    while(p->buf[offset] == '\n' || p->buf[offset] == '\r') {
+        offset++;
+    }
+    if(p->buf[offset] != '[') {
+        return 0;
+    }
+    offset++;
+    // Common metadata item but not exist in ff_lrc_metadata_conv
+    if(!memcmp(p->buf + offset, "offset:", 7)) {
+        return 40;
+    }
+    if(sscanf(p->buf + offset, "%"SCNd64":%"SCNu64".%"SCNu64"]",
+              &mm, &ss, &cs) == 3) {
+        return 50;
+    }
+    // Metadata items exist in ff_lrc_metadata_conv
+    for(metadata_item = ff_lrc_metadata_conv;
+        metadata_item->native; metadata_item++) {
+        size_t metadata_item_len = strlen(metadata_item->native);
+        if(p->buf[offset + metadata_item_len] == ':' &&
+           !memcmp(p->buf + offset, metadata_item->native, metadata_item_len)) {
+            return 40;
+        }
+    }
+    return 5; // Give it 5 scores since it starts with a bracket
+}
+
+static int lrc_read_header(AVFormatContext *s)
+{
+    LRCContext *lrc = s->priv_data;
+    AVBPrint line;
+    AVStream *st;
+
+    st = avformat_new_stream(s, NULL);
+    if(!st) {
+        return AVERROR(ENOMEM);
+    }
+    avpriv_set_pts_info(st, 64, 1, 1000);
+    lrc->ts_offset = 0;
+    st->codec->codec_type = AVMEDIA_TYPE_SUBTITLE;
+    st->codec->codec_id   = AV_CODEC_ID_TEXT;
+    av_bprint_init(&line, 0, AV_BPRINT_SIZE_UNLIMITED);
+
+    while(!url_feof(s->pb)) {
+        int64_t pos = read_line(&line, s->pb);
+        int64_t header_offset = find_header(line.str);
+        if(header_offset >= 0) {
+            char *comma_offset = strchr(line.str, ':');
+            if(comma_offset) {
+                char *right_bracket_offset = strchr(line.str, ']');
+                if(!right_bracket_offset) {
+                    continue;
+                }
+
+                *right_bracket_offset = *comma_offset = '\0';
+                if(strcmp(line.str + 1, "offset") ||
+                   sscanf(comma_offset + 1, "%"SCNd64, &lrc->ts_offset) != 1) {
+                    av_dict_set(&s->metadata, line.str + 1, comma_offset + 1, 0);
+                }
+                *comma_offset = ':';
+                *right_bracket_offset = ']';
+            }
+
+        } else {
+            AVPacket *sub;
+            int64_t ts_start = AV_NOPTS_VALUE;
+            int64_t ts_stroffset = 0;
+            int64_t ts_stroffset_incr = 0;
+            int64_t ts_strlength = count_ts(line.str);
+
+            while((ts_stroffset_incr = read_ts(line.str + ts_stroffset,
+                                               &ts_start)) != 0) {
+                ts_stroffset += ts_stroffset_incr;
+                sub = ff_subtitles_queue_insert(&lrc->q, line.str + ts_strlength,
+                                                line.len - ts_strlength, 0);
+                if(!sub) {
+                    return AVERROR(ENOMEM);
+                }
+                sub->pos = pos;
+                sub->pts = ts_start - lrc->ts_offset;
+                sub->duration = -1;
+            }
+        }
+    }
+    ff_subtitles_queue_finalize(&lrc->q);
+    ff_metadata_conv_ctx(s, NULL, ff_lrc_metadata_conv);
+    return 0;
+}
+
+static int lrc_read_packet(AVFormatContext *s, AVPacket *pkt)
+{
+    LRCContext *lrc = s->priv_data;
+    return ff_subtitles_queue_read_packet(&lrc->q, pkt);
+}
+
+static int lrc_read_seek(AVFormatContext *s, int stream_index,
+                         int64_t min_ts, int64_t ts, int64_t max_ts, int flags)
+{
+    LRCContext *lrc = s->priv_data;
+    return ff_subtitles_queue_seek(&lrc->q, s, stream_index,
+                                   min_ts, ts, max_ts, flags);
+}
+
+static int lrc_read_close(AVFormatContext *s)
+{
+    LRCContext *lrc = s->priv_data;
+    ff_subtitles_queue_clean(&lrc->q);
+    return 0;
+}
+
+AVInputFormat ff_lrc_demuxer = {
+    .name           = "lrc",
+    .long_name      = NULL_IF_CONFIG_SMALL("LRC lyrics"),
+    .priv_data_size = sizeof (LRCContext),
+    .read_probe     = lrc_probe,
+    .read_header    = lrc_read_header,
+    .read_packet    = lrc_read_packet,
+    .read_close     = lrc_read_close,
+    .read_seek2     = lrc_read_seek
+};
diff --git a/libavformat/lrcenc.c b/libavformat/lrcenc.c
new file mode 100644
index 0000000..9eb274f
--- /dev/null
+++ b/libavformat/lrcenc.c
@@ -0,0 +1,151 @@
+/*
+ * LRC lyrics file format decoder
+ * Copyright (c) 2014 StarBrilliant <m13...@hotmail.com>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg 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.
+ *
+ * FFmpeg 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 FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <inttypes.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "avformat.h"
+#include "internal.h"
+#include "lrc.h"
+#include "metadata.h"
+#include "subtitles.h"
+#include "version.h"
+#include "libavutil/bprint.h"
+#include "libavutil/dict.h"
+#include "libavutil/log.h"
+#include "libavutil/macros.h"
+
+static int lrc_write_header(AVFormatContext *s)
+{
+    const AVDictionaryEntry *metadata_item;
+
+    if(s->nb_streams != 1 ||
+       s->streams[0]->codec->codec_type != AVMEDIA_TYPE_SUBTITLE) {
+        av_log(s, AV_LOG_ERROR,
+               "LRC supports only a single subtitle stream.\n");
+        return AVERROR(EINVAL);
+    }
+    if(s->streams[0]->codec->codec_id != AV_CODEC_ID_SUBRIP &&
+       s->streams[0]->codec->codec_id != AV_CODEC_ID_TEXT) {
+        av_log(s, AV_LOG_ERROR, "Unsupported subtitle codec: %s\n",
+               avcodec_get_name(s->streams[0]->codec->codec_id));
+        return AVERROR(EINVAL);
+    }
+    avpriv_set_pts_info(s->streams[0], 64, 1, 100);
+
+    ff_metadata_conv_ctx(s, ff_lrc_metadata_conv, NULL);
+    if(!(s->flags & AVFMT_FLAG_BITEXACT)) { // avoid breaking regression tests
+        /* LRC provides a metadata slot for specifying encoder version
+         * in addition to encoder name. We will store LIBAVFORMAT_VERSION
+         * to it.
+         */
+        av_dict_set(&s->metadata, "ve", AV_STRINGIFY(LIBAVFORMAT_VERSION), 0);
+    } else {
+        av_dict_set(&s->metadata, "ve", NULL, 0);
+    }
+    for(metadata_item = NULL;
+       (metadata_item = av_dict_get(s->metadata, "", metadata_item,
+                                    AV_DICT_IGNORE_SUFFIX)) != 0;) {
+        char *delim;
+        if(!metadata_item->value[0]) {
+            continue;
+        }
+        while((delim = strchr(metadata_item->value, '\n')) != NULL) {
+            *delim = ' ';
+        }
+        while((delim = strchr(metadata_item->value, '\r')) != NULL) {
+            *delim = ' ';
+        }
+        avio_printf(s->pb, "[%s:%s]\n",
+                    metadata_item->key, metadata_item->value);
+    }
+    return 0;
+}
+
+static int lrc_write_packet(AVFormatContext *s, AVPacket *pkt)
+{
+    if(pkt->pts != AV_NOPTS_VALUE) {
+        char *data = av_malloc(pkt->size + 1);
+        char *line;
+        char *delim;
+
+        if(!data) {
+            return AVERROR(ENOMEM);
+        }
+        memcpy(data, pkt->data, pkt->size);
+        data[pkt->size] = '\0';
+
+        for(delim = data + pkt->size - 1;
+            delim >= data && (delim[0] == '\n' || delim[0] == '\r'); delim--) {
+            delim[0] = '\0'; // Strip last empty lines
+        }
+        line = data;
+        while(line[0] == '\n' || line[0] == '\r') {
+            line++; // Skip first empty lines
+        }
+
+        while(line) {
+            delim = strchr(line, '\n');
+            if(delim) {
+                if(delim > line && delim[-1] == '\r') {
+                    delim[-1] = '\0';
+                }
+                delim[0] = '\0';
+                delim++;
+            }
+            if(line[0] == '[') {
+                av_log(s, AV_LOG_WARNING,
+                       "Subtitle starts with '[', may cause problems with LRC format.\n");
+            }
+
+            if(pkt->pts >= 0) {
+                avio_printf(s->pb, "[%02"PRId64":%02"PRId64".%02"PRId64"]",
+                            (pkt->pts / 6000),
+                            ((pkt->pts / 100) % 60),
+                            (pkt->pts % 100));
+            } else {
+                /* Offset feature of LRC can easily make pts negative,
+                 * we just output it directly and let the player drop it. */
+                avio_printf(s->pb, "[-%02"PRId64":%02"PRId64".%02"PRId64"]",
+                            (-pkt->pts) / 6000,
+                            ((-pkt->pts) / 100) % 60,
+                            (-pkt->pts) % 100);
+            }
+            avio_printf(s->pb, "%s\n", line);
+            line = delim;
+        }
+        av_free(data);
+    }
+    return 0;
+}
+
+AVOutputFormat ff_lrc_muxer = {
+    .name           = "lrc",
+    .long_name      = NULL_IF_CONFIG_SMALL("LRC lyrics"),
+    .extensions     = "lrc",
+    .priv_data_size = 0,
+    .write_header   = lrc_write_header,
+    .write_packet   = lrc_write_packet,
+    .flags          = AVFMT_VARIABLE_FPS | AVFMT_GLOBALHEADER |
+                      AVFMT_TS_NEGATIVE | AVFMT_TS_NONSTRICT,
+    .subtitle_codec = AV_CODEC_ID_SUBRIP
+};
-- 
2.0.1

[ti:Swansong]
[al:Breadcrumbs]
[ar:Josh Woodward]
[length:04:21]
[by:StarBrilliant]
[00:35.62]I never thought I'd see the day
[00:38.42]I thought that I had finally moved along
[00:43.80]And I had let you go so long ago, so long
[00:49.26]This is not, this is not where I belong
[00:57.38]So I wait for this shallow itch to pass
[01:05.98]And I wait, yeah I wait
[01:12.14][02:22.72][03:08.05][03:30.81]Hey hey, I'm ok
[01:15.18][02:25.68][03:10.81][03:32.98]I don't need this anyway, I'm fine
[01:19.82][02:30.38][03:15.50][03:38.02]What's yours and mine
[01:23.64][02:34.12][03:19.31][03:41.91]Oh oh, I don't know
[01:26.44][02:36.98][03:22.11][03:44.54]What I was ever hoping I would find
[01:31.66][02:42.28][03:27.34][03:50.04]But it's time for me to leave this all 
behind
[01:46.20]I don't regret a single thing
[01:48.98]I couldn't say it didn't feel alright
[01:54.10]But I don't want to stay and I don't want to fight
[01:59.76]All alone, with my foolish appetite
[02:07.94]So I wait for this shallow itch to pass
[02:16.52]And I wait, yeah I wait
[02:45.36]I don't have the heart to give away to you again
[02:51.08]I don't have the stomach for it, no one ever wins
[02:56.70]We had our fun but I have sung this song to you before
[03:01.68]Here's my last refrain
[-00:01.02](This is a negative timestamp which should not make program crash)
[01:35.74][03:54.82]
_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
http://ffmpeg.org/mailman/listinfo/ffmpeg-devel

Reply via email to