summaryrefslogtreecommitdiff
path: root/libavformat/sga.c
diff options
context:
space:
mode:
authorPaul B Mahol <onemda@gmail.com>2021-02-21 13:33:07 +0100
committerPaul B Mahol <onemda@gmail.com>2021-03-01 17:41:05 +0100
commitc6c82428af51ab63068234a038564acfd4960eec (patch)
tree6bb3c5fa79460325bbe3cd78195d8b5239477edf /libavformat/sga.c
parent6b43e1a86c7c356be7b8fc553fa8721e502290e2 (diff)
avformat: add Digital Pictures SGA game demuxer
Diffstat (limited to 'libavformat/sga.c')
-rw-r--r--libavformat/sga.c477
1 files changed, 477 insertions, 0 deletions
diff --git a/libavformat/sga.c b/libavformat/sga.c
new file mode 100644
index 0000000000..957ca0f4cd
--- /dev/null
+++ b/libavformat/sga.c
@@ -0,0 +1,477 @@
+/*
+ * Digital Pictures SGA game demuxer
+ *
+ * Copyright (C) 2021 Paul B Mahol
+ *
+ * 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 "libavutil/intreadwrite.h"
+#include "libavutil/avassert.h"
+#include "libavutil/internal.h"
+#include "avformat.h"
+#include "internal.h"
+#include "avio_internal.h"
+
+#define SEGA_CD_PCM_NUM 12500000
+#define SEGA_CD_PCM_DEN 786432
+
+typedef struct SGADemuxContext {
+ int video_stream_index;
+ int audio_stream_index;
+
+ uint8_t sector[65536 * 2];
+ int sector_headers;
+ int sample_rate;
+ int first_audio_size;
+ int payload_size;
+ int packet_type;
+ int flags;
+ int idx;
+ int left;
+ int64_t pkt_pos;
+} SGADemuxContext;
+
+static int sga_probe(const AVProbeData *p)
+{
+ const uint8_t *src = p->buf;
+ int score = 0, sectors = 1;
+ int last_left = 0;
+ int sample_rate = -1;
+
+ if (p->buf_size < 2048)
+ return 0;
+
+ for (int i = 0; i + 2 < p->buf_size; i += 2048) {
+ int header = AV_RB16(src + i);
+
+ if ((header > 0x07FE && header < 0x8100) ||
+ (header > 0x8200 && header < 0xA100) ||
+ (header > 0xA200 && header < 0xC100)) {
+ sectors = 0;
+ break;
+ }
+ }
+
+ for (int i = 0; i + 4 < p->buf_size;) {
+ int header = AV_RB16(src + i);
+ int left = AV_RB16(src + i + 2);
+ int offset, type, size;
+
+ if (last_left < 0)
+ return 0;
+ if (sectors && header && last_left == 0) {
+ if (left <= 8)
+ return 0;
+ last_left = left;
+ } else if (sectors && header) {
+ left = header;
+ last_left -= left;
+ if (header != 0x7FE && left < 7)
+ return 0;
+ } else if (sectors) {
+ if (left <= 8)
+ return 0;
+ i += sectors ? 2048 : left + 4;
+ last_left = 0;
+ continue;
+ }
+
+ if (sectors && (i > 0 && left < 0x7fe) &&
+ (i + left + 14 < p->buf_size)) {
+ offset = i + left + 2;
+ } else if (sectors && i > 0) {
+ i += 2048;
+ last_left -= FFMIN(last_left, 2046);
+ continue;
+ } else {
+ offset = 0;
+ last_left = left;
+ }
+
+ header = AV_RB16(src + offset);
+ size = AV_RB16(src + offset + 2) + 4;
+
+ while ((header & 0xFF00) == 0) {
+ offset++;
+ if (offset + 4 >= p->buf_size)
+ break;
+ header = AV_RB16(src + offset);
+ size = AV_RB16(src + offset + 2) + 4;
+ }
+
+ if (offset + 12 >= p->buf_size)
+ break;
+ if ((header & 0xFF) > 1)
+ return 0;
+ type = header >> 8;
+
+ if (type == 0xAA ||
+ type == 0xA1 ||
+ type == 0xA2 ||
+ type == 0xA3) {
+ int new_rate;
+
+ if (size <= 12)
+ return 0;
+ new_rate = AV_RB16(src + offset + 8);
+ if (sample_rate < 0)
+ sample_rate = new_rate;
+ if (sample_rate == 0 || new_rate != sample_rate)
+ return 0;
+ if (src[offset + 10] != 1)
+ return 0;
+
+ score += 10;
+ } else if (type == 0xC1 ||
+ type == 0xC6 ||
+ type == 0xC7 ||
+ type == 0xC8 ||
+ type == 0xC9 ||
+ type == 0xCB ||
+ type == 0xCD ||
+ type == 0xE7) {
+ int nb_pals = src[offset + 9];
+ int tiles_w = src[offset + 10];
+ int tiles_h = src[offset + 11];
+
+ if (size <= 12)
+ return 0;
+ if (nb_pals == 0 || nb_pals > 4)
+ return 0;
+ if (tiles_w == 0 || tiles_w > 80)
+ return 0;
+ if (tiles_h == 0 || tiles_h > 60)
+ return 0;
+
+ score += 10;
+ } else if (header == 0x7FE) {
+ ;
+ } else {
+ return 0;
+ }
+
+ i += sectors ? 2048 : size + 4;
+ last_left -= FFMIN(last_left, 2046);
+
+ if (score < 0)
+ break;
+ }
+
+ return av_clip(score, 0, AVPROBE_SCORE_MAX);
+}
+
+static int sga_read_header(AVFormatContext *s)
+{
+ SGADemuxContext *sga = s->priv_data;
+ AVIOContext *pb = s->pb;
+
+ sga->sector_headers = 1;
+ sga->first_audio_size = 0;
+ sga->video_stream_index = -1;
+ sga->audio_stream_index = -1;
+ sga->left = 2048;
+ sga->idx = 0;
+
+ s->ctx_flags |= AVFMTCTX_NOHEADER;
+
+ if (pb->seekable & AVIO_SEEKABLE_NORMAL) {
+ while (!avio_feof(pb)) {
+ int header = avio_rb16(pb);
+ int type = header >> 8;
+ int skip = 2046;
+ int clock;
+
+ if (!sga->first_audio_size &&
+ (type == 0xAA ||
+ type == 0xA1 ||
+ type == 0xA2 ||
+ type == 0xA3)) {
+ sga->first_audio_size = avio_rb16(pb);
+ avio_skip(pb, 4);
+ clock = avio_rb16(pb);
+ sga->sample_rate = av_rescale(clock,
+ SEGA_CD_PCM_NUM,
+ SEGA_CD_PCM_DEN);
+ skip -= 8;
+ }
+ if ((header > 0x07FE && header < 0x8100) ||
+ (header > 0x8200 && header < 0xA100) ||
+ (header > 0xA200 && header < 0xC100)) {
+ sga->sector_headers = 0;
+ break;
+ }
+
+ avio_skip(pb, skip);
+ }
+
+ avio_seek(pb, 0, SEEK_SET);
+ }
+
+ return 0;
+}
+
+static void print_stats(AVFormatContext *s, const char *where)
+{
+ SGADemuxContext *sga = s->priv_data;
+
+ av_log(s, AV_LOG_DEBUG, "START %s\n", where);
+ av_log(s, AV_LOG_DEBUG, "pos: %lX\n", avio_tell(s->pb));
+ av_log(s, AV_LOG_DEBUG, "idx: %X\n", sga->idx);
+ av_log(s, AV_LOG_DEBUG, "packet_type: %X\n", sga->packet_type);
+ av_log(s, AV_LOG_DEBUG, "payload_size: %X\n", sga->payload_size);
+ av_log(s, AV_LOG_DEBUG, "SECTOR: %016lX\n", AV_RB64(sga->sector));
+ av_log(s, AV_LOG_DEBUG, "stream: %X\n", sga->sector[1]);
+ av_log(s, AV_LOG_DEBUG, "END %s\n", where);
+}
+
+static void update_type_size(AVFormatContext *s)
+{
+ SGADemuxContext *sga = s->priv_data;
+
+ if (sga->idx >= 4) {
+ sga->packet_type = sga->sector[0];
+ sga->payload_size = AV_RB16(sga->sector + 2);
+ } else {
+ sga->packet_type = 0;
+ sga->payload_size = 0;
+ }
+}
+
+static int sga_video_packet(AVFormatContext *s, AVPacket *pkt)
+{
+ SGADemuxContext *sga = s->priv_data;
+ int ret;
+
+ if (sga->payload_size <= 8)
+ return AVERROR_INVALIDDATA;
+
+ if (sga->video_stream_index == -1) {
+ AVRational frame_rate;
+
+ AVStream *st = avformat_new_stream(s, NULL);
+ if (!st)
+ return AVERROR(ENOMEM);
+
+ st->start_time = 0;
+ st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
+ st->codecpar->codec_tag = 0;
+ st->codecpar->codec_id = AV_CODEC_ID_SGA_VIDEO;
+ sga->video_stream_index = st->index;
+
+ if (sga->first_audio_size > 0 && sga->sample_rate > 0) {
+ frame_rate.num = sga->sample_rate;
+ frame_rate.den = sga->first_audio_size;
+ } else {
+ frame_rate.num = 15;
+ frame_rate.den = 1;
+ }
+ avpriv_set_pts_info(st, 64, frame_rate.den, frame_rate.num);
+ }
+
+ ret = av_new_packet(pkt, sga->payload_size + 4);
+ if (ret < 0)
+ return AVERROR(ENOMEM);
+ memcpy(pkt->data, sga->sector, sga->payload_size + 4);
+ av_assert0(sga->idx >= sga->payload_size + 4);
+ memmove(sga->sector, sga->sector + sga->payload_size + 4, sga->idx - sga->payload_size - 4);
+
+ pkt->stream_index = sga->video_stream_index;
+ pkt->duration = 1;
+ pkt->pos = sga->pkt_pos;
+ pkt->flags |= sga->flags;
+ sga->idx -= sga->payload_size + 4;
+ sga->flags = 0;
+ update_type_size(s);
+
+ av_log(s, AV_LOG_DEBUG, "VIDEO PACKET: %d:%016lX i:%X\n", pkt->size, AV_RB64(sga->sector), sga->idx);
+
+ return 0;
+}
+
+static int sga_audio_packet(AVFormatContext *s, AVPacket *pkt)
+{
+ SGADemuxContext *sga = s->priv_data;
+ int ret;
+
+ if (sga->payload_size <= 8)
+ return AVERROR_INVALIDDATA;
+
+ if (sga->audio_stream_index == -1) {
+ AVStream *st = avformat_new_stream(s, NULL);
+ if (!st)
+ return AVERROR(ENOMEM);
+
+ st->start_time = 0;
+ st->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
+ st->codecpar->codec_tag = 0;
+ st->codecpar->codec_id = AV_CODEC_ID_PCM_SGA;
+ st->codecpar->channels = 1;
+ st->codecpar->channel_layout= AV_CH_LAYOUT_MONO;
+ st->codecpar->sample_rate = av_rescale(AV_RB16(sga->sector + 8),
+ SEGA_CD_PCM_NUM,
+ SEGA_CD_PCM_DEN);
+ sga->audio_stream_index = st->index;
+
+ avpriv_set_pts_info(st, 64, 1, st->codecpar->sample_rate);
+ }
+
+ ret = av_new_packet(pkt, sga->payload_size - 8);
+ if (ret < 0)
+ return AVERROR(ENOMEM);
+ memcpy(pkt->data, sga->sector + 12, sga->payload_size - 8);
+ av_assert0(sga->idx >= sga->payload_size + 4);
+ memmove(sga->sector, sga->sector + sga->payload_size + 4, sga->idx - sga->payload_size - 4);
+
+ pkt->stream_index = sga->audio_stream_index;
+ pkt->duration = pkt->size;
+ pkt->pos = sga->pkt_pos;
+ pkt->flags |= sga->flags;
+ sga->idx -= sga->payload_size + 4;
+ sga->flags = 0;
+ update_type_size(s);
+
+ av_log(s, AV_LOG_DEBUG, "AUDIO PACKET: %d:%016lX i:%X\n", pkt->size, AV_RB64(sga->sector), sga->idx);
+
+ return 0;
+}
+
+static int sga_packet(AVFormatContext *s, AVPacket *pkt)
+{
+ SGADemuxContext *sga = s->priv_data;
+ int ret = 0;
+
+ if (sga->packet_type == 0xCD ||
+ sga->packet_type == 0xCB ||
+ sga->packet_type == 0xC9 ||
+ sga->packet_type == 0xC8 ||
+ sga->packet_type == 0xC7 ||
+ sga->packet_type == 0xC6 ||
+ sga->packet_type == 0xC1 ||
+ sga->packet_type == 0xE7) {
+ ret = sga_video_packet(s, pkt);
+ } else if (sga->packet_type == 0xA1 ||
+ sga->packet_type == 0xA2 ||
+ sga->packet_type == 0xA3 ||
+ sga->packet_type == 0xAA) {
+ ret = sga_audio_packet(s, pkt);
+ } else {
+ if (sga->idx == 0)
+ return AVERROR_EOF;
+ if (sga->sector[0])
+ return AVERROR_INVALIDDATA;
+ memmove(sga->sector, sga->sector + 1, sga->idx - 1);
+ sga->idx--;
+ return AVERROR(EAGAIN);
+ }
+
+ return ret;
+}
+
+static int try_packet(AVFormatContext *s, AVPacket *pkt)
+{
+ SGADemuxContext *sga = s->priv_data;
+ int ret = AVERROR(EAGAIN);
+
+ update_type_size(s);
+ if (sga->idx >= sga->payload_size + 4) {
+ print_stats(s, "before sga_packet");
+ ret = sga_packet(s, pkt);
+ print_stats(s, "after sga_packet");
+ if (ret != AVERROR(EAGAIN))
+ return ret;
+ }
+
+ return sga->idx < sga->payload_size + 4 ? AVERROR(EAGAIN) : ret;
+}
+
+static int sga_read_packet(AVFormatContext *s, AVPacket *pkt)
+{
+ SGADemuxContext *sga = s->priv_data;
+ AVIOContext *pb = s->pb;
+ int header, ret = 0;
+
+ sga->pkt_pos = avio_tell(pb);
+
+retry:
+ update_type_size(s);
+
+ print_stats(s, "start");
+ if (avio_feof(pb) &&
+ (!sga->payload_size || sga->idx < sga->payload_size + 4))
+ return AVERROR_EOF;
+
+ if (sga->idx < sga->payload_size + 4) {
+ ret = ffio_ensure_seekback(pb, 2);
+ if (ret < 0)
+ return ret;
+
+ print_stats(s, "before read header");
+ header = avio_rb16(pb);
+ if (!header) {
+ avio_skip(pb, 2046);
+ sga->left = 0;
+ } else if (!avio_feof(pb) &&
+ ((header >> 15) ||
+ !sga->sector_headers)) {
+ avio_seek(pb, -2, SEEK_CUR);
+ sga->flags = AV_PKT_FLAG_KEY;
+ sga->left = 2048;
+ } else {
+ sga->left = 2046;
+ }
+
+ av_assert0(sga->idx + sga->left < sizeof(sga->sector));
+ ret = avio_read(pb, sga->sector + sga->idx, sga->left);
+ if (ret > 0)
+ sga->idx += ret;
+ else if (ret != AVERROR_EOF && ret)
+ return ret;
+ print_stats(s, "after read header");
+
+ update_type_size(s);
+ }
+
+ ret = try_packet(s, pkt);
+ if (ret == AVERROR(EAGAIN))
+ goto retry;
+
+ return ret;
+}
+
+static int sga_seek(AVFormatContext *s, int stream_index,
+ int64_t timestamp, int flags)
+{
+ SGADemuxContext *sga = s->priv_data;
+
+ sga->packet_type = sga->payload_size = sga->idx = 0;
+ memset(sga->sector, 0, sizeof(sga->sector));
+
+ return -1;
+}
+
+AVInputFormat ff_sga_demuxer = {
+ .name = "sga",
+ .long_name = NULL_IF_CONFIG_SMALL("Digital Pictures SGA"),
+ .priv_data_size = sizeof(SGADemuxContext),
+ .read_probe = sga_probe,
+ .read_header = sga_read_header,
+ .read_packet = sga_read_packet,
+ .read_seek = sga_seek,
+ .extensions = "sga",
+ .flags = AVFMT_GENERIC_INDEX,
+};