mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-27 12:11:13 +00:00
1292 lines
32 KiB
C
1292 lines
32 KiB
C
/*
|
|
* libzvbi -- Raw VBI decoder
|
|
*
|
|
* Copyright (C) 2000-2004 Michael H. Schimek
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Library General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This library 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
|
|
* Library General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Library General Public
|
|
* License along with this library; if not, write to the
|
|
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
* Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
/* $Id: raw_decoder.c,v 1.24 2008-08-19 10:04:46 mschimek Exp $ */
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
# include "config.h"
|
|
#endif
|
|
|
|
#include <assert.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
|
|
#include "misc.h"
|
|
#include "raw_decoder.h"
|
|
|
|
#ifndef RAW_DECODER_PATTERN_DUMP
|
|
# define RAW_DECODER_PATTERN_DUMP 0
|
|
#endif
|
|
|
|
# define sp_sample_format sampling_format
|
|
|
|
/**
|
|
* $addtogroup RawDecoder Raw VBI decoder
|
|
* $ingroup Raw
|
|
* $brief Converting a raw VBI image to sliced VBI data.
|
|
*/
|
|
|
|
/* Missing:
|
|
VITC PAL 6-22 11.2us 1.8125 Mbit NRZ two start bits + CRC
|
|
VITC NTSC 10-21 ditto
|
|
CGMS NTSC 20 11us .450450 Mbit NRZ ?
|
|
MOJI
|
|
*/
|
|
const _vbi_service_par _vbi_service_table[] = {
|
|
{
|
|
VBI_SLICED_TELETEXT_A, /* UNTESTED */
|
|
"Teletext System A",
|
|
VBI_VIDEOSTD_SET_625_50,
|
|
{6, 318},
|
|
{22, 335},
|
|
10500, 6203125, 6203125, /* 397 x FH */
|
|
0x00AAAAE7, 0xFFFF, 18, 6, 37 * 8, VBI_MODULATION_NRZ_LSB,
|
|
0, /* probably */
|
|
}, {
|
|
VBI_SLICED_TELETEXT_B_L10_625,
|
|
"Teletext System B 625 Level 1.5",
|
|
VBI_VIDEOSTD_SET_625_50,
|
|
{7, 320},
|
|
{22, 335},
|
|
10300, 6937500, 6937500, /* 444 x FH */
|
|
0x00AAAAE4, 0xFFFF, 18, 6, 42 * 8, VBI_MODULATION_NRZ_LSB,
|
|
0,
|
|
}, {
|
|
VBI_SLICED_TELETEXT_B,
|
|
"Teletext System B, 625",
|
|
VBI_VIDEOSTD_SET_625_50,
|
|
{6, 318},
|
|
{22, 335},
|
|
10300, 6937500, 6937500, /* 444 x FH */
|
|
0x00AAAAE4, 0xFFFF, 18, 6, 42 * 8, VBI_MODULATION_NRZ_LSB,
|
|
0,
|
|
}, {
|
|
VBI_SLICED_TELETEXT_C_625, /* UNTESTED */
|
|
"Teletext System C 625",
|
|
VBI_VIDEOSTD_SET_625_50,
|
|
{6, 318},
|
|
{22, 335},
|
|
10480, 5734375, 5734375, /* 367 x FH */
|
|
0x00AAAAE7, 0xFFFF, 18, 6, 33 * 8, VBI_MODULATION_NRZ_LSB,
|
|
0,
|
|
}, {
|
|
VBI_SLICED_TELETEXT_D_625, /* UNTESTED */
|
|
"Teletext System D 625",
|
|
VBI_VIDEOSTD_SET_625_50,
|
|
{6, 318},
|
|
{22, 335},
|
|
10500, /* or 10970 depending on field order */
|
|
5642787, 5642787, /* 14/11 x FSC (color subcarrier) */
|
|
0x00AAAAE5, 0xFFFF, 18, 6, 34 * 8, VBI_MODULATION_NRZ_LSB,
|
|
0,
|
|
}, {
|
|
VBI_SLICED_VPS, "Video Program System",
|
|
VBI_VIDEOSTD_SET_PAL_BG,
|
|
{16, 0},
|
|
{16, 0},
|
|
12500, 5000000, 2500000, /* 160 x FH */
|
|
0xAAAA8A99, 0xFFFFFF, 32, 0, 13 * 8,
|
|
VBI_MODULATION_BIPHASE_MSB,
|
|
_VBI_SP_FIELD_NUM,
|
|
}, {
|
|
VBI_SLICED_VPS_F2, "Pseudo-VPS on field 2",
|
|
VBI_VIDEOSTD_SET_PAL_BG,
|
|
{0, 329},
|
|
{0, 329},
|
|
12500, 5000000, 2500000, /* 160 x FH */
|
|
0xAAAA8A99, 0xFFFFFF, 32, 0, 13 * 8,
|
|
VBI_MODULATION_BIPHASE_MSB,
|
|
_VBI_SP_FIELD_NUM,
|
|
}, {
|
|
VBI_SLICED_WSS_625, "Wide Screen Signalling 625",
|
|
VBI_VIDEOSTD_SET_625_50,
|
|
{23, 0},
|
|
{23, 0},
|
|
11000, 5000000, 833333, /* 160/3 x FH */
|
|
/* ...1000 111 / 0 0011 1100 0111 1000 0011 111x */
|
|
/* ...0010 010 / 0 1001 1001 0011 0011 1001 110x */
|
|
0x8E3C783E, 0x2499339C, 32, 0, 14 * 1,
|
|
VBI_MODULATION_BIPHASE_LSB,
|
|
/* Hm. Too easily confused with caption?? */
|
|
_VBI_SP_FIELD_NUM | _VBI_SP_LINE_NUM,
|
|
}, {
|
|
VBI_SLICED_CAPTION_625_F1, "Closed Caption 625, field 1",
|
|
VBI_VIDEOSTD_SET_625_50,
|
|
{22, 0},
|
|
{22, 0},
|
|
10500, 1000000, 500000, /* 32 x FH */
|
|
0x00005551, 0x7FF, 14, 2, 2 * 8, VBI_MODULATION_NRZ_LSB,
|
|
_VBI_SP_FIELD_NUM,
|
|
}, {
|
|
VBI_SLICED_CAPTION_625_F2, "Closed Caption 625, field 2",
|
|
VBI_VIDEOSTD_SET_625_50,
|
|
{0, 335},
|
|
{0, 335},
|
|
10500, 1000000, 500000, /* 32 x FH */
|
|
0x00005551, 0x7FF, 14, 2, 2 * 8, VBI_MODULATION_NRZ_LSB,
|
|
_VBI_SP_FIELD_NUM,
|
|
}, {
|
|
VBI_SLICED_VBI_625, "VBI 625", /* Blank VBI */
|
|
VBI_VIDEOSTD_SET_625_50,
|
|
{6, 318},
|
|
{22, 335},
|
|
10000, 1510000, 1510000,
|
|
0, 0, 0, 0, 10 * 8, 0, /* 10.0-2 ... 62.9+1 us */
|
|
0,
|
|
}, {
|
|
VBI_SLICED_TELETEXT_B_525, /* UNTESTED */
|
|
"Teletext System B 525",
|
|
VBI_VIDEOSTD_SET_525_60,
|
|
{10, 272},
|
|
{21, 284},
|
|
10500, 5727272, 5727272, /* 364 x FH */
|
|
0x00AAAAE4, 0xFFFF, 18, 6, 34 * 8, VBI_MODULATION_NRZ_LSB,
|
|
0,
|
|
}, {
|
|
VBI_SLICED_TELETEXT_C_525, /* UNTESTED */
|
|
"Teletext System C 525",
|
|
VBI_VIDEOSTD_SET_525_60,
|
|
{10, 272},
|
|
{21, 284},
|
|
10480, 5727272, 5727272, /* 364 x FH */
|
|
0x00AAAAE7, 0xFFFF, 18, 6, 33 * 8, VBI_MODULATION_NRZ_LSB,
|
|
0,
|
|
}, {
|
|
VBI_SLICED_TELETEXT_D_525, /* UNTESTED */
|
|
"Teletext System D 525",
|
|
VBI_VIDEOSTD_SET_525_60,
|
|
{10, 272},
|
|
{21, 284},
|
|
9780, 5727272, 5727272, /* 364 x FH */
|
|
0x00AAAAE5, 0xFFFF, 18, 6, 34 * 8, VBI_MODULATION_NRZ_LSB,
|
|
0,
|
|
}, {
|
|
#if 0 /* FIXME probably wrong */
|
|
VBI_SLICED_WSS_CPR1204, /* NOT CONFIRMED (EIA-J CPR-1204) */
|
|
"Wide Screen Signalling 525",
|
|
VBI_VIDEOSTD_SET_NTSC_M_JP,
|
|
{20, 283},
|
|
{20, 283},
|
|
11200, 1789773, 447443, /* 1/8 x FSC */
|
|
0x000000F0, 0xFF, 8, 0, 20 * 1, VBI_MODULATION_NRZ_MSB,
|
|
/* No useful FRC, but a six bit CRC */
|
|
0,
|
|
}, {
|
|
#endif
|
|
VBI_SLICED_CAPTION_525_F1,
|
|
"Closed Caption 525, field 1",
|
|
VBI_VIDEOSTD_SET_525_60,
|
|
{21, 0},
|
|
{21, 0},
|
|
10500, 1006976, 503488, /* 32 x FH */
|
|
#if 0
|
|
/* Test of CRI bits has been removed to handle the
|
|
incorrect signal observed by Rich Kandel (see
|
|
_VBI_RAW_SHIFT_CC_CRI). */
|
|
0x03, 0x0F, 4, 0, 2 * 8, VBI_MODULATION_NRZ_LSB,
|
|
#else
|
|
0x00005551, 0x7FF, 14, 2, 2 * 8, VBI_MODULATION_NRZ_LSB,
|
|
/* I've seen CC signals on other lines and there's no
|
|
way to distinguish from the transmitted data. */
|
|
#endif
|
|
_VBI_SP_FIELD_NUM | _VBI_SP_LINE_NUM,
|
|
}, {
|
|
VBI_SLICED_CAPTION_525_F2,
|
|
"Closed Caption 525, field 2",
|
|
VBI_VIDEOSTD_SET_525_60,
|
|
{0, 284},
|
|
{0, 284},
|
|
10500, 1006976, 503488, /* 32 x FH */
|
|
#if 0
|
|
/* Test of CRI bits has been removed to handle the
|
|
incorrect signal observed by Rich Kandel (see
|
|
_VBI_RAW_SHIFT_CC_CRI). */
|
|
0x03, 0x0F, 4, 0, 2 * 8, VBI_MODULATION_NRZ_LSB,
|
|
#else
|
|
0x00005551, 0x7FF, 14, 2, 2 * 8, VBI_MODULATION_NRZ_LSB,
|
|
#endif
|
|
_VBI_SP_FIELD_NUM | _VBI_SP_LINE_NUM,
|
|
}, {
|
|
VBI_SLICED_2xCAPTION_525, /* NOT CONFIRMED */
|
|
"2xCaption 525",
|
|
VBI_VIDEOSTD_SET_525_60,
|
|
{10, 0},
|
|
{21, 0},
|
|
10500, 1006976, 1006976, /* 64 x FH */
|
|
0x000554ED, 0xFFFF, 12, 8, 4 * 8,
|
|
VBI_MODULATION_NRZ_LSB, /* Tb. */
|
|
_VBI_SP_FIELD_NUM,
|
|
}, {
|
|
VBI_SLICED_VBI_525, "VBI 525", /* Blank VBI */
|
|
VBI_VIDEOSTD_SET_525_60,
|
|
{10, 272},
|
|
{21, 284},
|
|
9500, 1510000, 1510000,
|
|
0, 0, 0, 0, 10 * 8, 0, /* 9.5-1 ... 62.4+1 us */
|
|
0,
|
|
}, {
|
|
0, NULL,
|
|
VBI_VIDEOSTD_SET_EMPTY,
|
|
{0, 0},
|
|
{0, 0},
|
|
0, 0, 0,
|
|
0, 0, 0, 0, 0, 0,
|
|
0,
|
|
}
|
|
};
|
|
|
|
_vbi_inline const _vbi_service_par *
|
|
find_service_par (unsigned int service)
|
|
{
|
|
unsigned int i;
|
|
|
|
for (i = 0; _vbi_service_table[i].id; ++i)
|
|
if (service == _vbi_service_table[i].id)
|
|
return _vbi_service_table + i;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* $ingroup Sliced
|
|
* $param service A data service identifier, for example from a
|
|
* vbi_sliced structure.
|
|
*
|
|
* $return
|
|
* Name of the $a service, in ASCII, or $c NULL if unknown.
|
|
*/
|
|
const char *
|
|
vbi_sliced_name (vbi_service_set service)
|
|
{
|
|
const _vbi_service_par *par;
|
|
|
|
/* These are ambiguous */
|
|
if (service == VBI_SLICED_CAPTION_525)
|
|
return "Closed Caption 525";
|
|
if (service == VBI_SLICED_CAPTION_625)
|
|
return "Closed Caption 625";
|
|
if (service == (VBI_SLICED_VPS | VBI_SLICED_VPS_F2))
|
|
return "Video Program System";
|
|
if (service == VBI_SLICED_TELETEXT_B_L25_625)
|
|
return "Teletext System B 625 Level 2.5";
|
|
|
|
/* Incorrect, no longer in table */
|
|
if (service == VBI_SLICED_TELETEXT_BD_525)
|
|
return "Teletext System B/D";
|
|
|
|
if ((par = find_service_par (service)))
|
|
return par->label;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* @ingroup Sliced
|
|
* @param service A data service identifier, for example from a
|
|
* vbi_sliced structure.
|
|
*
|
|
* @return
|
|
* Number of payload bits, @c 0 if the service is unknown.
|
|
*/
|
|
unsigned int
|
|
vbi_sliced_payload_bits (unsigned int service)
|
|
{
|
|
const _vbi_service_par *par;
|
|
|
|
/* These are ambiguous */
|
|
if (service == VBI_SLICED_CAPTION_525)
|
|
return 16;
|
|
if (service == VBI_SLICED_CAPTION_625)
|
|
return 16;
|
|
if (service == (VBI_SLICED_VPS | VBI_SLICED_VPS_F2))
|
|
return 13 * 8;
|
|
if (service == VBI_SLICED_TELETEXT_B_L25_625)
|
|
return 42 * 8;
|
|
|
|
/* Incorrect, no longer in table */
|
|
if (service == VBI_SLICED_TELETEXT_BD_525)
|
|
return 34 * 8;
|
|
|
|
if ((par = find_service_par (service)))
|
|
return par->payload;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
dump_pattern_line (const vbi3_raw_decoder * rd, unsigned int row, FILE * fp)
|
|
{
|
|
const vbi_sampling_par *sp;
|
|
unsigned int line;
|
|
unsigned int i;
|
|
|
|
sp = &rd->sampling;
|
|
|
|
if (sp->interlaced) {
|
|
unsigned int field = row & 1;
|
|
|
|
if (0 == sp->start[field])
|
|
line = 0;
|
|
else
|
|
line = sp->start[field] + (row >> 1);
|
|
} else {
|
|
if (row >= (unsigned int) sp->count[0]) {
|
|
if (0 == sp->start[1])
|
|
line = 0;
|
|
else
|
|
line = sp->start[1] + row - sp->count[0];
|
|
} else {
|
|
if (0 == sp->start[0])
|
|
line = 0;
|
|
else
|
|
line = sp->start[0] + row;
|
|
}
|
|
}
|
|
|
|
fprintf (fp, "scan line %3u: ", line);
|
|
|
|
for (i = 0; i < _VBI3_RAW_DECODER_MAX_WAYS; ++i) {
|
|
unsigned int pos;
|
|
|
|
pos = row * _VBI3_RAW_DECODER_MAX_WAYS;
|
|
fprintf (fp, "%02x ", (uint8_t) rd->pattern[pos + i]);
|
|
}
|
|
|
|
fputc ('\n', fp);
|
|
}
|
|
|
|
void
|
|
_vbi3_raw_decoder_dump (const vbi3_raw_decoder * rd, FILE * fp)
|
|
{
|
|
const vbi_sampling_par *sp;
|
|
unsigned int i;
|
|
|
|
assert (NULL != fp);
|
|
|
|
fprintf (fp, "vbi3_raw_decoder %p\n", rd);
|
|
|
|
if (NULL == rd)
|
|
return;
|
|
|
|
fprintf (fp, " services 0x%08x\n", rd->services);
|
|
|
|
for (i = 0; i < rd->n_jobs; ++i)
|
|
fprintf (fp, " job %u: 0x%08x (%s)\n",
|
|
i + 1, rd->jobs[i].id, vbi_sliced_name (rd->jobs[i].id));
|
|
|
|
if (!rd->pattern) {
|
|
fprintf (fp, " no pattern\n");
|
|
return;
|
|
}
|
|
|
|
sp = &rd->sampling;
|
|
|
|
for (i = 0; i < ((unsigned int) sp->count[0]
|
|
+ (unsigned int) sp->count[1]); ++i) {
|
|
fputs (" ", fp);
|
|
dump_pattern_line (rd, i, fp);
|
|
}
|
|
}
|
|
|
|
#if 0 /* @UNUSED */
|
|
_vbi_inline int
|
|
cpr1204_crc (const vbi_sliced * sliced)
|
|
{
|
|
const int poly = (1 << 6) + (1 << 1) + 1;
|
|
int crc, i;
|
|
|
|
crc = (+(sliced->data[0] << 12)
|
|
+ (sliced->data[1] << 4)
|
|
+ (sliced->data[2]));
|
|
|
|
crc |= (((1 << 6) - 1) << (14 + 6));
|
|
|
|
for (i = 14 + 6 - 1; i >= 0; i--) {
|
|
if (crc & ((1 << 6) << i))
|
|
crc ^= poly << i;
|
|
}
|
|
|
|
return crc;
|
|
}
|
|
#endif
|
|
|
|
static vbi_bool
|
|
slice (vbi3_raw_decoder * rd,
|
|
vbi_sliced * sliced,
|
|
_vbi3_raw_decoder_job * job, unsigned int i, const uint8_t * raw)
|
|
{
|
|
if (rd->debug && NULL != rd->sp_lines) {
|
|
return vbi3_bit_slicer_slice_with_points
|
|
(&job->slicer,
|
|
sliced->data,
|
|
sizeof (sliced->data),
|
|
rd->sp_lines[i].points,
|
|
&rd->sp_lines[i].n_points, N_ELEMENTS (rd->sp_lines[i].points), raw);
|
|
} else {
|
|
return vbi3_bit_slicer_slice
|
|
(&job->slicer, sliced->data, sizeof (sliced->data), raw);
|
|
}
|
|
}
|
|
|
|
_vbi_inline vbi_sliced *
|
|
decode_pattern (vbi3_raw_decoder * rd,
|
|
vbi_sliced * sliced, int8_t * pattern, unsigned int i, const uint8_t * raw)
|
|
{
|
|
vbi_sampling_par *sp;
|
|
int8_t *pat;
|
|
|
|
sp = &rd->sampling;
|
|
|
|
for (pat = pattern;; ++pat) {
|
|
int j;
|
|
|
|
j = *pat; /* data service n, blank 0, or counter -n */
|
|
|
|
if (j > 0) {
|
|
_vbi3_raw_decoder_job *job;
|
|
|
|
job = rd->jobs + j - 1;
|
|
|
|
if (!slice (rd, sliced, job, i, raw)) {
|
|
continue; /* no match, try next data service */
|
|
}
|
|
|
|
/* FIXME probably wrong */
|
|
if (0 && VBI_SLICED_WSS_CPR1204 == job->id) {
|
|
const int poly = (1 << 6) + (1 << 1) + 1;
|
|
int crc, j;
|
|
|
|
crc = (sliced->data[0] << 12)
|
|
+ (sliced->data[1] << 4)
|
|
+ sliced->data[2];
|
|
crc |= (((1 << 6) - 1) << (14 + 6));
|
|
|
|
for (j = 14 + 6 - 1; j >= 0; j--) {
|
|
if (crc & ((1 << 6) << j))
|
|
crc ^= poly << j;
|
|
}
|
|
|
|
if (crc)
|
|
continue; /* no match */
|
|
}
|
|
|
|
/* Positive match, output decoded line. */
|
|
|
|
/* FIXME: if we have a field number we should
|
|
really only set the service id of one field. */
|
|
sliced->id = job->id;
|
|
sliced->line = 0;
|
|
|
|
if (i >= (unsigned int) sp->count[0]) {
|
|
if (sp->synchronous && 0 != sp->start[1])
|
|
sliced->line = sp->start[1]
|
|
+ i - sp->count[0];
|
|
} else {
|
|
if (sp->synchronous && 0 != sp->start[0])
|
|
sliced->line = sp->start[0] + i;
|
|
}
|
|
|
|
if (0)
|
|
fprintf (stderr, "%2d %s\n",
|
|
sliced->line, vbi_sliced_name (sliced->id));
|
|
|
|
++sliced;
|
|
|
|
/* Predict line as non-blank, force testing for
|
|
all data services in the next 128 frames. */
|
|
pattern[_VBI3_RAW_DECODER_MAX_WAYS - 1] = -128;
|
|
} else if (pat == pattern) {
|
|
/* Line was predicted as blank, once in 16
|
|
frames look for data services. */
|
|
if (0 == rd->readjust) {
|
|
unsigned int size;
|
|
|
|
size = sizeof (*pattern)
|
|
* (_VBI3_RAW_DECODER_MAX_WAYS - 1);
|
|
|
|
j = pattern[0];
|
|
memmove (&pattern[0], &pattern[1], size);
|
|
pattern[_VBI3_RAW_DECODER_MAX_WAYS - 1] = j;
|
|
}
|
|
|
|
break;
|
|
} else if ((j = pattern[_VBI3_RAW_DECODER_MAX_WAYS - 1]) < 0) {
|
|
/* Increment counter, when zero predict line as
|
|
blank and stop looking for data services until
|
|
0 == rd->readjust. */
|
|
/* Disabled because we may miss caption/subtitles
|
|
when the signal inserter is disabled during silent
|
|
periods for more than 4-5 seconds. */
|
|
/* pattern[_VBI3_RAW_DECODER_MAX_WAYS - 1] = j + 1; */
|
|
break;
|
|
} else {
|
|
/* found nothing, j = 0 */
|
|
}
|
|
|
|
/* Try the found data service first next time. */
|
|
*pat = pattern[0];
|
|
pattern[0] = j;
|
|
|
|
break; /* line done */
|
|
}
|
|
|
|
return sliced;
|
|
}
|
|
|
|
/**
|
|
* $param rd Pointer to vbi3_raw_decoder object allocated with
|
|
* vbi3_raw_decoder_new().
|
|
* $param sliced Buffer to store the decoded vbi_sliced data. Since every
|
|
* vbi scan line may contain data, this should be an array of vbi_sliced
|
|
* with the same number of elements as scan lines in the raw image
|
|
* (vbi_sampling_parameters.count[0] + .count[1]).
|
|
* $param max_lines Size of $a sliced data array, in lines, not bytes.
|
|
* $param raw A raw vbi image as described by the vbi_sampling_par
|
|
* associated with $a rd.
|
|
*
|
|
* Decodes a raw vbi image, consisting of several scan lines of raw vbi data,
|
|
* to sliced vbi data. The output is sorted by ascending line number.
|
|
*
|
|
* Note this function attempts to learn which lines carry which data
|
|
* service, or if any, to speed up decoding. You should avoid using the same
|
|
* vbi3_raw_decoder object for different sources.
|
|
*
|
|
* $return
|
|
* The number of lines decoded, i. e. the number of vbi_sliced records
|
|
* written.
|
|
*/
|
|
unsigned int
|
|
vbi3_raw_decoder_decode (vbi3_raw_decoder * rd,
|
|
vbi_sliced * sliced, unsigned int max_lines, const uint8_t * raw)
|
|
{
|
|
vbi_sampling_par *sp;
|
|
unsigned int scan_lines;
|
|
unsigned int pitch;
|
|
int8_t *pattern;
|
|
const uint8_t *raw1;
|
|
vbi_sliced *sliced_begin;
|
|
vbi_sliced *sliced_end;
|
|
unsigned int i;
|
|
|
|
if (!rd->services)
|
|
return 0;
|
|
|
|
sp = &rd->sampling;
|
|
|
|
scan_lines = sp->count[0] + sp->count[1];
|
|
pitch = sp->bytes_per_line << sp->interlaced;
|
|
|
|
pattern = rd->pattern;
|
|
|
|
raw1 = raw;
|
|
|
|
sliced_begin = sliced;
|
|
sliced_end = sliced + max_lines;
|
|
|
|
if (RAW_DECODER_PATTERN_DUMP)
|
|
_vbi3_raw_decoder_dump (rd, stderr);
|
|
|
|
for (i = 0; i < scan_lines; ++i) {
|
|
if (sliced >= sliced_end)
|
|
break;
|
|
|
|
if (sp->interlaced && i == (unsigned int) sp->count[0])
|
|
raw = raw1 + sp->bytes_per_line;
|
|
|
|
sliced = decode_pattern (rd, sliced, pattern, i, raw);
|
|
|
|
pattern += _VBI3_RAW_DECODER_MAX_WAYS;
|
|
raw += pitch;
|
|
}
|
|
|
|
rd->readjust = (rd->readjust + 1) & 15;
|
|
|
|
return sliced - sliced_begin;
|
|
}
|
|
|
|
/**
|
|
* $param rd Pointer to vbi3_raw_decoder object allocated with
|
|
* vbi3_raw_decoder_new().
|
|
*
|
|
* Resets a vbi3_raw_decoder object, removing all services added
|
|
* with vbi3_raw_decoder_add_services().
|
|
*/
|
|
void
|
|
vbi3_raw_decoder_reset (vbi3_raw_decoder * rd)
|
|
{
|
|
assert (NULL != rd);
|
|
|
|
if (rd->pattern) {
|
|
vbi_free (rd->pattern);
|
|
rd->pattern = NULL;
|
|
}
|
|
|
|
rd->services = 0;
|
|
rd->n_jobs = 0;
|
|
|
|
rd->readjust = 1;
|
|
|
|
CLEAR (rd->jobs);
|
|
}
|
|
|
|
static void
|
|
remove_job_from_pattern (vbi3_raw_decoder * rd, int job_num)
|
|
{
|
|
int8_t *pattern;
|
|
unsigned int scan_lines;
|
|
|
|
job_num += 1; /* index into rd->jobs, 0 means no job */
|
|
|
|
pattern = rd->pattern;
|
|
scan_lines = rd->sampling.count[0] + rd->sampling.count[1];
|
|
|
|
/* For each scan line. */
|
|
while (scan_lines-- > 0) {
|
|
int8_t *dst;
|
|
int8_t *src;
|
|
int8_t *end;
|
|
|
|
dst = pattern;
|
|
end = pattern + _VBI3_RAW_DECODER_MAX_WAYS;
|
|
|
|
/* Remove jobs with job_num, fill up pattern with 0.
|
|
Jobs above job_num move down in rd->jobs. */
|
|
for (src = dst; src < end; ++src) {
|
|
int8_t num = *src;
|
|
|
|
if (num > job_num)
|
|
*dst++ = num - 1;
|
|
else if (num != job_num)
|
|
*dst++ = num;
|
|
}
|
|
|
|
while (dst < end)
|
|
*dst++ = 0;
|
|
|
|
pattern = end;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* $param rd Pointer to vbi3_raw_decoder object allocated with
|
|
* vbi3_raw_decoder_new().
|
|
* $param services Set of data services.
|
|
*
|
|
* Removes one or more data services to be decoded from the
|
|
* vbi3_raw_decoder object.
|
|
*
|
|
* $return
|
|
* Set describing the remaining data services $a rd will decode.
|
|
*/
|
|
vbi_service_set
|
|
vbi3_raw_decoder_remove_services
|
|
(vbi3_raw_decoder * rd, vbi_service_set services) {
|
|
_vbi3_raw_decoder_job *job;
|
|
unsigned int job_num;
|
|
|
|
assert (NULL != rd);
|
|
|
|
job = rd->jobs;
|
|
job_num = 0;
|
|
|
|
while (job_num < rd->n_jobs) {
|
|
if (job->id & services) {
|
|
if (rd->pattern)
|
|
remove_job_from_pattern (rd, job_num);
|
|
|
|
memmove (job, job + 1, (rd->n_jobs - job_num - 1) * sizeof (*job));
|
|
|
|
--rd->n_jobs;
|
|
|
|
CLEAR (rd->jobs[rd->n_jobs]);
|
|
} else {
|
|
++job_num;
|
|
}
|
|
}
|
|
|
|
rd->services &= ~services;
|
|
|
|
return rd->services;
|
|
}
|
|
|
|
static vbi_bool
|
|
add_job_to_pattern (vbi3_raw_decoder * rd,
|
|
int job_num, unsigned int *start, unsigned int *count)
|
|
{
|
|
int8_t *pattern_end;
|
|
unsigned int scan_lines;
|
|
unsigned int field;
|
|
|
|
job_num += 1; /* index into rd->jobs, 0 means no job */
|
|
|
|
scan_lines = rd->sampling.count[0]
|
|
+ rd->sampling.count[1];
|
|
|
|
pattern_end = rd->pattern + scan_lines * _VBI3_RAW_DECODER_MAX_WAYS;
|
|
|
|
for (field = 0; field < 2; ++field) {
|
|
int8_t *pattern;
|
|
unsigned int i;
|
|
|
|
pattern = rd->pattern + start[field] * _VBI3_RAW_DECODER_MAX_WAYS;
|
|
|
|
/* For each line where we may find the data. */
|
|
for (i = 0; i < count[field]; ++i) {
|
|
unsigned int free;
|
|
int8_t *dst;
|
|
int8_t *src;
|
|
int8_t *end;
|
|
|
|
assert (pattern < pattern_end);
|
|
|
|
dst = pattern;
|
|
end = pattern + _VBI3_RAW_DECODER_MAX_WAYS;
|
|
|
|
free = 0;
|
|
|
|
for (src = dst; src < end; ++src) {
|
|
int8_t num = *src;
|
|
|
|
if (num <= 0) {
|
|
++free;
|
|
continue;
|
|
} else {
|
|
free += (num == job_num);
|
|
*dst++ = num;
|
|
}
|
|
}
|
|
|
|
while (dst < end)
|
|
*dst++ = 0;
|
|
|
|
if (free <= 1) /* reserve a NULL way */
|
|
return FALSE;
|
|
|
|
pattern = end;
|
|
}
|
|
}
|
|
|
|
for (field = 0; field < 2; ++field) {
|
|
int8_t *pattern;
|
|
unsigned int i;
|
|
|
|
pattern = rd->pattern + start[field] * _VBI3_RAW_DECODER_MAX_WAYS;
|
|
|
|
/* For each line where we may find the data. */
|
|
for (i = 0; i < count[field]; ++i) {
|
|
unsigned int way;
|
|
|
|
for (way = 0; pattern[way] > 0; ++way)
|
|
if (pattern[way] == job_num)
|
|
break;
|
|
|
|
pattern[way] = job_num;
|
|
pattern[_VBI3_RAW_DECODER_MAX_WAYS - 1] = -128;
|
|
|
|
pattern += _VBI3_RAW_DECODER_MAX_WAYS;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
lines_containing_data (unsigned int start[2],
|
|
unsigned int count[2],
|
|
const vbi_sampling_par * sp, const _vbi_service_par * par)
|
|
{
|
|
unsigned int field;
|
|
|
|
start[0] = 0;
|
|
start[1] = sp->count[0];
|
|
|
|
count[0] = sp->count[0];
|
|
count[1] = sp->count[1];
|
|
|
|
if (!sp->synchronous) {
|
|
/* XXX Scanning all lines isn't always necessary. */
|
|
return;
|
|
}
|
|
|
|
for (field = 0; field < 2; ++field) {
|
|
unsigned int first;
|
|
unsigned int last;
|
|
|
|
if (0 == par->first[field]
|
|
|| 0 == par->last[field]) {
|
|
/* No data on this field. */
|
|
count[field] = 0;
|
|
continue;
|
|
}
|
|
|
|
first = sp->start[field];
|
|
last = first + sp->count[field] - 1;
|
|
|
|
if (first > 0 && sp->count[field] > 0) {
|
|
assert (par->first[field] <= par->last[field]);
|
|
|
|
if ((unsigned int) par->first[field] > last
|
|
|| (unsigned int) par->last[field] < first)
|
|
continue;
|
|
|
|
first = MAX (first, (unsigned int) par->first[field]);
|
|
last = MIN ((unsigned int) par->last[field], last);
|
|
|
|
start[field] += first - sp->start[field];
|
|
count[field] = last + 1 - first;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* $param rd Pointer to vbi3_raw_decoder object allocated with
|
|
* vbi3_raw_decoder_new().
|
|
* $param services Set of data services.
|
|
* $param strict A value of 0, 1 or 2 requests loose, reliable or strict
|
|
* matching of sampling parameters. For example if the data service
|
|
* requires knowledge of line numbers, $c 0 will always accept the
|
|
* service (which may work if the scan lines are populated in a
|
|
* non-confusing way) but $c 1 or $c 2 will not. If the data service
|
|
* might use more lines than are sampled, $c 1 will accept but $c 2
|
|
* will not. If unsure, set to $c 1.
|
|
*
|
|
* Adds one or more data services to be decoded. Currently the libzvbi
|
|
* raw vbi decoder can decode up to eight data services in parallel.
|
|
*
|
|
* $return
|
|
* Set describing the data services $a rd will decode. The function
|
|
* eliminates services which cannot be decoded with the current
|
|
* sampling parameters, or when they exceed the decoder capacity.
|
|
*/
|
|
/* Attn: strict must be int for compatibility with libzvbi 0.2 (-1 == 0) */
|
|
vbi_service_set
|
|
vbi3_raw_decoder_add_services (vbi3_raw_decoder * rd,
|
|
vbi_service_set services, int strict)
|
|
{
|
|
const _vbi_service_par *par;
|
|
double min_offset;
|
|
|
|
assert (NULL != rd);
|
|
|
|
services &= ~(VBI_SLICED_VBI_525 | VBI_SLICED_VBI_625);
|
|
|
|
if (rd->services & services) {
|
|
info (&rd->log,
|
|
"Already decoding services 0x%08x.", rd->services & services);
|
|
services &= ~rd->services;
|
|
}
|
|
|
|
if (0 == services) {
|
|
info (&rd->log, "No services to add.");
|
|
return rd->services;
|
|
}
|
|
|
|
if (!rd->pattern) {
|
|
unsigned int scan_lines;
|
|
unsigned int scan_ways;
|
|
unsigned int size;
|
|
|
|
scan_lines = rd->sampling.count[0] + rd->sampling.count[1];
|
|
scan_ways = scan_lines * _VBI3_RAW_DECODER_MAX_WAYS;
|
|
|
|
size = scan_ways * sizeof (rd->pattern[0]);
|
|
rd->pattern = (int8_t *) vbi_malloc (size);
|
|
if (NULL == rd->pattern) {
|
|
error (&rd->log, "Out of memory.");
|
|
return rd->services;
|
|
}
|
|
|
|
memset (rd->pattern, 0, scan_ways * sizeof (rd->pattern[0]));
|
|
}
|
|
|
|
if (525 == rd->sampling.scanning) {
|
|
min_offset = 7.9e-6;
|
|
} else {
|
|
min_offset = 8.0e-6;
|
|
}
|
|
|
|
for (par = _vbi_service_table; par->id; ++par) {
|
|
vbi_sampling_par *sp;
|
|
_vbi3_raw_decoder_job *job;
|
|
unsigned int start[2];
|
|
unsigned int count[2];
|
|
unsigned int sample_offset;
|
|
unsigned int samples_per_line;
|
|
unsigned int cri_end;
|
|
unsigned int j;
|
|
|
|
if (0 == (par->id & services))
|
|
continue;
|
|
|
|
job = rd->jobs;
|
|
|
|
/* Some jobs can be merged, otherwise we add a new job. */
|
|
for (j = 0; j < rd->n_jobs; ++j) {
|
|
unsigned int id = job->id | par->id;
|
|
|
|
/* Level 1.0 and 2.5 */
|
|
if (0 == (id & ~VBI_SLICED_TELETEXT_B)
|
|
/* Field 1 and 2 */
|
|
|| 0 == (id & ~VBI_SLICED_CAPTION_525)
|
|
|| 0 == (id & ~VBI_SLICED_CAPTION_625)
|
|
|| 0 == (id & ~(VBI_SLICED_VPS | VBI_SLICED_VPS_F2)))
|
|
break;
|
|
|
|
++job;
|
|
}
|
|
|
|
if (j >= _VBI3_RAW_DECODER_MAX_JOBS) {
|
|
error (&rd->log,
|
|
"Set 0x%08x exceeds number of "
|
|
"simultaneously decodable "
|
|
"services (%u).", services, _VBI3_RAW_DECODER_MAX_WAYS);
|
|
break;
|
|
} else if (j >= rd->n_jobs) {
|
|
job->id = 0;
|
|
}
|
|
|
|
|
|
sp = &rd->sampling;
|
|
|
|
if (!_vbi_sampling_par_check_services_log (sp, par->id, strict, &rd->log))
|
|
continue;
|
|
|
|
|
|
sample_offset = 0;
|
|
|
|
/* Skip color burst. */
|
|
/* Offsets aren't that reliable, sigh. */
|
|
if (0 && sp->offset > 0 && strict > 0) {
|
|
double offset;
|
|
|
|
offset = sp->offset / (double) sp->sampling_rate;
|
|
if (offset < min_offset)
|
|
sample_offset = (int) (min_offset * sp->sampling_rate);
|
|
}
|
|
|
|
if (VBI_SLICED_WSS_625 & par->id) {
|
|
/* TODO: WSS 625 occupies only first half of line,
|
|
we can abort earlier. */
|
|
cri_end = ~0;
|
|
} else {
|
|
cri_end = ~0;
|
|
}
|
|
|
|
samples_per_line = sp->bytes_per_line
|
|
/ VBI_PIXFMT_BPP (sp->sp_sample_format);
|
|
|
|
if (!_vbi3_bit_slicer_init (&job->slicer)) {
|
|
assert (!"bit_slicer_init");
|
|
}
|
|
|
|
if (!vbi3_bit_slicer_set_params
|
|
(&job->slicer,
|
|
sp->sp_sample_format,
|
|
sp->sampling_rate,
|
|
sample_offset,
|
|
samples_per_line,
|
|
par->cri_frc >> par->frc_bits,
|
|
par->cri_frc_mask >> par->frc_bits,
|
|
par->cri_bits,
|
|
par->cri_rate,
|
|
cri_end,
|
|
(par->cri_frc & ((1U << par->frc_bits) - 1)),
|
|
par->frc_bits, par->payload, par->bit_rate,
|
|
(vbi3_modulation) par->modulation)) {
|
|
assert (!"bit_slicer_set_params");
|
|
}
|
|
|
|
vbi3_bit_slicer_set_log_fn (&job->slicer,
|
|
rd->log.mask, rd->log.fn, rd->log.user_data);
|
|
|
|
lines_containing_data (start, count, sp, par);
|
|
|
|
if (!add_job_to_pattern (rd, job - rd->jobs, start, count)) {
|
|
error (&rd->log,
|
|
"Out of decoder pattern space for "
|
|
"service 0x%08x (%s).", par->id, par->label);
|
|
continue;
|
|
}
|
|
|
|
job->id |= par->id;
|
|
|
|
if (job >= rd->jobs + rd->n_jobs)
|
|
++rd->n_jobs;
|
|
|
|
rd->services |= par->id;
|
|
}
|
|
|
|
return rd->services;
|
|
}
|
|
|
|
vbi_bool
|
|
vbi3_raw_decoder_sampling_point (vbi3_raw_decoder * rd,
|
|
vbi3_bit_slicer_point * point, unsigned int row, unsigned int nth_bit)
|
|
{
|
|
assert (NULL != rd);
|
|
assert (NULL != point);
|
|
|
|
if (row >= rd->n_sp_lines)
|
|
return FALSE;
|
|
|
|
if (nth_bit >= rd->sp_lines[row].n_points)
|
|
return FALSE;
|
|
|
|
*point = rd->sp_lines[row].points[nth_bit];
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
vbi_bool
|
|
vbi3_raw_decoder_debug (vbi3_raw_decoder * rd, vbi_bool enable)
|
|
{
|
|
#if 0 /* Set but unused */
|
|
_vbi3_raw_decoder_sp_line *sp_lines;
|
|
#endif
|
|
unsigned int n_lines;
|
|
vbi_bool r;
|
|
|
|
assert (NULL != rd);
|
|
|
|
#if 0 /* Set but unused */
|
|
sp_lines = NULL;
|
|
#endif
|
|
r = TRUE;
|
|
|
|
rd->debug = !!enable;
|
|
|
|
n_lines = 0;
|
|
if (enable) {
|
|
n_lines = rd->sampling.count[0] + rd->sampling.count[1];
|
|
}
|
|
|
|
switch (rd->sampling.sp_sample_format) {
|
|
case VBI_PIXFMT_YUV420:
|
|
break;
|
|
|
|
default:
|
|
/* Not implemented. */
|
|
n_lines = 0;
|
|
r = FALSE;
|
|
break;
|
|
}
|
|
|
|
if (rd->n_sp_lines == n_lines)
|
|
return r;
|
|
|
|
vbi_free (rd->sp_lines);
|
|
rd->sp_lines = NULL;
|
|
rd->n_sp_lines = 0;
|
|
|
|
if (n_lines > 0) {
|
|
rd->sp_lines = calloc (n_lines, sizeof (*rd->sp_lines));
|
|
if (NULL == rd->sp_lines)
|
|
return FALSE;
|
|
|
|
rd->n_sp_lines = n_lines;
|
|
}
|
|
|
|
return r;
|
|
}
|
|
|
|
vbi_service_set
|
|
vbi3_raw_decoder_services (vbi3_raw_decoder * rd)
|
|
{
|
|
assert (NULL != rd);
|
|
|
|
return rd->services;
|
|
}
|
|
|
|
/**
|
|
* $param rd Pointer to a vbi3_raw_decoder object allocated with
|
|
* vbi3_raw_decoder_new().
|
|
* $param sp New sampling parameters.
|
|
* $param strict See vbi3_raw_decoder_add_services().
|
|
*
|
|
* Changes the sampling parameters used by $a rd. This will
|
|
* remove all services which have been added with
|
|
* vbi3_raw_decoder_add_services() but cannot be decoded with
|
|
* the new sampling parameters.
|
|
*
|
|
* $return
|
|
* Set of data services $rd will be decode after the change.
|
|
* Can be zero if the sampling parameters are invalid or some
|
|
* other error occurred.
|
|
*/
|
|
/* Attn: strict must be int for compatibility with libzvbi 0.2 (-1 == 0) */
|
|
vbi_service_set
|
|
vbi3_raw_decoder_set_sampling_par
|
|
(vbi3_raw_decoder * rd, const vbi_sampling_par * sp, int strict)
|
|
{
|
|
unsigned int services;
|
|
|
|
assert (NULL != rd);
|
|
assert (NULL != sp);
|
|
|
|
services = rd->services;
|
|
|
|
vbi3_raw_decoder_reset (rd);
|
|
|
|
if (!_vbi_sampling_par_valid_log (sp, &rd->log)) {
|
|
CLEAR (rd->sampling);
|
|
return 0;
|
|
}
|
|
|
|
rd->sampling = *sp;
|
|
|
|
/* Error ignored. */
|
|
vbi3_raw_decoder_debug (rd, rd->debug);
|
|
|
|
return vbi3_raw_decoder_add_services (rd, services, strict);
|
|
}
|
|
|
|
/**
|
|
* $param rd Pointer to a vbi3_raw_decoder object allocated with
|
|
* vbi3_raw_decoder_new().
|
|
* $param sp Sampling parameters will be stored here.
|
|
*
|
|
* Returns sampling parameters used by $a rd.
|
|
*/
|
|
void vbi3_raw_decoder_get_sampling_par
|
|
(const vbi3_raw_decoder * rd, vbi_sampling_par * sp)
|
|
{
|
|
assert (NULL != rd);
|
|
assert (NULL != sp);
|
|
|
|
*sp = rd->sampling;
|
|
}
|
|
|
|
void
|
|
vbi3_raw_decoder_set_log_fn (vbi3_raw_decoder * rd,
|
|
vbi_log_fn * log_fn, void *user_data, vbi_log_mask mask)
|
|
{
|
|
unsigned int i;
|
|
|
|
assert (NULL != rd);
|
|
|
|
if (NULL == log_fn)
|
|
mask = 0;
|
|
|
|
rd->log.mask = mask;
|
|
rd->log.fn = log_fn;
|
|
rd->log.user_data = user_data;
|
|
|
|
for (i = 0; i < _VBI3_RAW_DECODER_MAX_JOBS; ++i) {
|
|
vbi3_bit_slicer_set_log_fn (&rd->jobs[i].slicer, mask, log_fn, user_data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*
|
|
* Free all resources associated with @a rd.
|
|
*/
|
|
void
|
|
_vbi3_raw_decoder_destroy (vbi3_raw_decoder * rd)
|
|
{
|
|
vbi3_raw_decoder_reset (rd);
|
|
|
|
vbi3_raw_decoder_debug (rd, FALSE);
|
|
|
|
/* Make unusable. */
|
|
CLEAR (*rd);
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*
|
|
* See vbi3_raw_decoder_new().
|
|
*/
|
|
vbi_bool
|
|
_vbi3_raw_decoder_init (vbi3_raw_decoder * rd, const vbi_sampling_par * sp)
|
|
{
|
|
CLEAR (*rd);
|
|
|
|
vbi3_raw_decoder_reset (rd);
|
|
|
|
if (NULL != sp) {
|
|
if (!_vbi_sampling_par_valid_log (sp, &rd->log))
|
|
return FALSE;
|
|
|
|
rd->sampling = *sp;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* $param rd Pointer to a vbi3_raw_decoder object allocated with
|
|
* vbi3_raw_decoder_new(), can be NULL
|
|
*
|
|
* Deletes a vbi3_raw_decoder object.
|
|
*/
|
|
void
|
|
vbi3_raw_decoder_delete (vbi3_raw_decoder * rd)
|
|
{
|
|
if (NULL == rd)
|
|
return;
|
|
|
|
_vbi3_raw_decoder_destroy (rd);
|
|
|
|
vbi_free (rd);
|
|
}
|
|
|
|
/**
|
|
* $param sp VBI sampling parameters describing the raw VBI image
|
|
* to decode, can be $c NULL. If they are negotiatable you can determine
|
|
* suitable parameters with vbi_sampling_par_from_services(). You can
|
|
* change the sampling parameters later with
|
|
* vbi3_raw_decoder_set_sampling_par().
|
|
*
|
|
* Allocates a vbi3_raw_decoder object. To actually decode data
|
|
* services you must request the data with vbi3_raw_decoder_add_services().
|
|
*
|
|
* $returns
|
|
* NULL when out of memory or the sampling parameters are invalid,
|
|
* Otherwise a pointer to an opaque vbi3_raw_decoder object which must
|
|
* be deleted with vbi3_raw_decoder_delete() when done.
|
|
*/
|
|
vbi3_raw_decoder *
|
|
vbi3_raw_decoder_new (const vbi_sampling_par * sp)
|
|
{
|
|
vbi3_raw_decoder *rd;
|
|
|
|
rd = vbi_malloc (sizeof (*rd));
|
|
if (NULL == rd) {
|
|
errno = ENOMEM;
|
|
return NULL;
|
|
}
|
|
|
|
if (!_vbi3_raw_decoder_init (rd, sp)) {
|
|
vbi_free (rd);
|
|
rd = NULL;
|
|
}
|
|
|
|
return rd;
|
|
}
|
|
|
|
/*
|
|
Local variables:
|
|
c-set-style: K&R
|
|
c-basic-offset: 8
|
|
End:
|
|
*/
|