[FFmpeg-devel] [PATCH] AVFormat: LRC demuxer and muxer

Star Brilliant m13253 at hotmail.com
Wed Jul 9 15:29:15 CEST 2014


LRC is widely used to represent music lyrics.
It is useful to have lavf decode LRC during playback or transcode it to 
other subtitle formats.

I know there once was a rejected patch here:
http://ffmpeg.org/pipermail/ffmpeg-devel/2010-December/084342.html
So this is why I rewrote another implementation.

This demuxer and muxer implementation supports multiple timestamp tags 
and offset handling, which does not exist in Peter Ross's one mentioned 
above.


 From 64686e1d8e9dd77ad1830f715d5a0485c8d81aa9 Mon Sep 17 00:00:00 2001
From: Star Brilliant <m13253 at 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.h        |  38 ++++++++
  libavformat/lrcdec.c     | 220 
+++++++++++++++++++++++++++++++++++++++++++++++
  libavformat/lrcenc.c     | 132 ++++++++++++++++++++++++++++
  7 files changed, 395 insertions(+)
  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
+ at 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..05f5fcd 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 subtitles.o
+OBJS-$(CONFIG_LRC_MUXER)                 += lrcenc.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.h b/libavformat/lrc.h
new file mode 100644
index 0000000..831a9cf
--- /dev/null
+++ b/libavformat/lrc.h
@@ -0,0 +1,38 @@
+/*
+ * LRC lyrics file format decoder
+ * Copyright (c) 2014 StarBrilliant <m13253 at 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"
+
+static const AVMetadataConv ffpriv_lrc_metadata_conv[] = {
+    {"ti", "title"},
+    {"al", "album"},
+    {"ar", "artist"},
+    {"au", "author"},
+    {"by", "creator"},
+    {"re", "encoder"},
+    {"ve", "encoder_version"},
+    {0, 0}
+};
+
+#endif
diff --git a/libavformat/lrcdec.c b/libavformat/lrcdec.c
new file mode 100644
index 0000000..cf9f956
--- /dev/null
+++ b/libavformat/lrcdec.c
@@ -0,0 +1,220 @@
+/*
+ * LRC lyrics file format decoder
+ * Copyright (c) 2014 StarBrilliant <m13253 at 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 <stdint.h>
+#include <string.h>
+
+#include "avformat.h"
+#include "internal.h"
+#include "lrc.h"
+#include "metadata.h"
+#include "subtitles.h"
+#include "libavcodec/internal.h"
+#include "libavutil/bprint.h"
+#include "libavutil/dict.h"
+
+typedef struct LRCContext {
+    FFDemuxSubtitlesQueue q;
+    int ts_offset;
+} LRCContext;
+
+static int64_t find_header(const char *p)
+{
+    int64_t offset = 0;
+    while(p[offset] &&
+          p[offset] == ' ' || p[offset] == '\t')
+        offset++;
+    if(p[offset] && 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;
+
+    while(p[offset])
+        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;
+    int mm, ss, cs;
+
+    while(p[offset] == ' ' || p[offset] == '\t')
+        offset++;
+    if(p[offset] != '[')
+        return 0;
+    if(sscanf(p, "[-%d:%d.%d]", &mm, &ss, &cs) == 3)
+        *start = -(mm*60000LL + ss*1000LL + cs*10LL); // just in case 
negative pts
+    else if(sscanf(p, "[%d:%d.%d]", &mm, &ss, &cs) == 3)
+        *start = mm*60000LL + ss*1000LL + cs*10LL;
+    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;
+    int mm, ss, cs;
+    char metadata_buffer[3];
+
+    if(!memcmp(p->buf, "\xef\xbb\xbf", 3)) // Skip UTF-8 BOM header
+        offset += 3;
+    if(!memcmp(p->buf + offset, "[ti:", 4) ||
+       !memcmp(p->buf + offset, "[al:", 4) ||
+       !memcmp(p->buf + offset, "[ar:", 4))
+        return AVPROBE_SCORE_MAX;
+    if(sscanf(p->buf + offset, "[%d:%d.%d]", &mm, &ss, &cs) == 3)
+        return AVPROBE_SCORE_MAX-10;
+    if(sscanf(p->buf + offset, "[%2[a-z]:", metadata_buffer))
+        return AVPROBE_SCORE_MAX-20;
+    if(p->buf[0] == '[')
+        return 10;
+    return 0;
+}
+
+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, "%d", &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*1LL;
+                sub->duration = -1;
+            }
+        }
+    }
+    ff_subtitles_queue_finalize(&lrc->q);
+    ff_metadata_conv_ctx(s, NULL, ffpriv_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..325adc8
--- /dev/null
+++ b/libavformat/lrcenc.c
@@ -0,0 +1,132 @@
+/*
+ * LRC lyrics file format decoder
+ * Copyright (c) 2014 StarBrilliant <m13253 at 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 <stdint.h>
+#include <string.h>
+
+#include "avformat.h"
+#include "internal.h"
+#include "lrc.h"
+#include "metadata.h"
+#include "subtitles.h"
+#include "version.h"
+#include "libavcodec/internal.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, ffpriv_lrc_metadata_conv, NULL);
+    av_dict_set(&s->metadata, "ve", AV_STRINGIFY(LIBAVFORMAT_VERSION), 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, "[%02d:%02d.%02d]",
+                            (int) (pkt->pts / 6000LL),
+                            (int) ((pkt->pts / 100LL) % 60LL),
+                            (int) (pkt->pts % 100LL));
+            else // just in case negative pts, output something meaningful
+                avio_printf(s->pb, "[-%02d:%02d.%02d]",
+                            (int) ((-pkt->pts) / 6000LL),
+                            (int) (((-pkt->pts) / 100LL) % 60LL),
+                            (int) ((-pkt->pts) % 100LL));
+            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



More information about the ffmpeg-devel mailing list