Index: libs/libmythtv/vbi608extractor.h =================================================================== --- libs/libmythtv/vbi608extractor.h (revision 0) +++ libs/libmythtv/vbi608extractor.h (revision 0) @@ -0,0 +1,57 @@ +/* + VBI 608 Extractor, extracts CEA-608 VBI from a line of raw data. + Copyright (C) 2010 Digital Nirvana, Inc. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#ifndef _VBI_608_EXTRACTOR_H_ +#define _VBI_608_EXTRACTOR_H_ + +#include + +#include + +#include "frame.h" + +class VBI608Extractor +{ + public: + VBI608Extractor(); + + uint16_t GetCode1(void) const { return code[0]; } + uint16_t GetCode2(void) const { return code[1]; } + + bool ExtractCC(const VideoFrame*, uint max_lines = 4); + bool ExtractCC12(const unsigned char *buf, uint width); + bool ExtractCC34(const unsigned char *buf, uint width); + + uint FillCCData(uint8_t cc_data[8]); + + private: + float GetClockStart(void) const { return start; } + float GetClockRate(void) const { return rate; } + bool FindClocks(const unsigned char *buf, uint width); + + QList raw_minimas; + QList raw_maximas; + QList maximas; + QList minimas; + float start; + float rate; + uint16_t code[2]; +}; + +#endif // _VBI_608_EXTRACTOR_H_ Index: libs/libmythtv/NuppelVideoRecorder.cpp =================================================================== --- libs/libmythtv/NuppelVideoRecorder.cpp (revision 25079) +++ libs/libmythtv/NuppelVideoRecorder.cpp (working copy) @@ -24,13 +24,13 @@ #include "mythcontext.h" #include "mythverbose.h" #include "NuppelVideoRecorder.h" -#include "vbitext/cc.h" #include "channelbase.h" #include "filtermanager.h" #include "recordingprofile.h" #include "tv_rec.h" #include "tv_play.h" #include "audioinput.h" +#include "vbi608extractor.h" #ifdef WORDS_BIGENDIAN extern "C" { @@ -181,6 +181,7 @@ volume = 100; ccr.SetEnabled(true); + vbi608 = new VBI608Extractor(); ccd = new CC608Decoder(&ccr); go7007 = false; @@ -253,6 +254,8 @@ delete videoFilters; if (FiltMan) delete FiltMan; + if (vbi608) + delete vbi608; if (ccd) delete ccd; } @@ -2573,7 +2576,7 @@ void NuppelVideoRecorder::FormatTeletextSubtitles(struct VBIData *vbidata) {} #endif // USING_V4L -void NuppelVideoRecorder::FormatCC(struct cc *cc) +void NuppelVideoRecorder::FormatCC(uint code1, uint code2) { struct timeval tnow; gettimeofday (&tnow, &tzone); @@ -2583,7 +2586,7 @@ int tc = (tnow.tv_sec - stm.tv_sec) * 1000 + tnow.tv_usec / 1000 - stm.tv_usec / 1000; - ccd->FormatCC(tc, cc->code1, cc->code2); + ccd->FormatCC(tc, code1, code2); } void NuppelVideoRecorder::AddTextData(unsigned char *buf, int len, @@ -2651,10 +2654,9 @@ //VERBOSE(VB_IMPORTANT, LOC + "vbi begin"); struct VBIData vbicallbackdata; struct vbi *pal_tt = NULL; - struct cc *ntsc_cc = NULL; int vbifd = -1; - char *ptr = NULL; - char *ptr_end = NULL; + unsigned char *buf = NULL, *ptr = NULL, *ptr_end = NULL; + uint width = 0, start_line = 0, line_count = 0; QByteArray vbidev = vbidevice.toAscii(); if (VBIMode::PAL_TT == vbimode) @@ -2669,17 +2671,7 @@ } else if (VBIMode::NTSC_CC == vbimode) { - ntsc_cc = new struct cc; - bzero(ntsc_cc, sizeof(struct cc)); - ntsc_cc->fd = open(vbidev.constData(), O_RDONLY|O_NONBLOCK); - ntsc_cc->code1 = -1; - ntsc_cc->code2 = -1; - - vbifd = ntsc_cc->fd; - ptr = ntsc_cc->buffer; - - if (vbifd < 0) - delete ntsc_cc; + vbifd = open(vbidev.constData(), O_RDONLY|O_NONBLOCK); } else { @@ -2703,7 +2695,7 @@ { VERBOSE(VB_IMPORTANT, LOC_ERR + "Failed to query vbi capabilities (v4l1)"); - cc_close(ntsc_cc); + close(vbifd); return; } VERBOSE(VB_RECORD, LOC + "vbi_format rate: "<samples_per_line = vfmt.samples_per_line; - ntsc_cc->start_line = vfmt.start[0]; - ntsc_cc->line_count = vfmt.count[0]; - ntsc_cc->scale0 = (vfmt.sampling_rate + 503488 / 2) / 503488; - ntsc_cc->scale1 = (ntsc_cc->scale0 * 2 + 3) / 5; /* 40% */ - ptr_end = ntsc_cc->buffer + sz; - if (sz > CC_VBIBUFSIZE) + buf = ptr = new unsigned char[sz]; + ptr_end = buf + sz; + width = vfmt.samples_per_line; + start_line = vfmt.start[0]; + line_count = vfmt.count[0]; + if (line_count != vfmt.count[1]) { VERBOSE(VB_IMPORTANT, LOC_ERR + - "VBI format has too many samples per frame"); - cc_close(ntsc_cc); + "VBI must have the same number of " + "odd and even fields for our decoder"); + delete [] buf; + close(vbifd); return; } + if (start_line > 21 || start_line + line_count < 22) + { + VERBOSE(VB_IMPORTANT, LOC_ERR + + "VBI does not include line 21"); + delete [] buf; + close(vbifd); + return; + } } // Qt4 requires a QMutex as a parameter... @@ -2774,9 +2775,21 @@ ptr = (ret > 0) ? ptr + ret : ptr; if ((ptr_end - ptr) == 0) { - cc_decode(ntsc_cc); - FormatCC(ntsc_cc); - ptr = ntsc_cc->buffer; + unsigned char *line21_field1 = + ptr + (21 - start_line) * width; + unsigned char *line21_field2 = + ptr + (line_count + 21 - start_line) * width; + bool cc1 = vbi608->ExtractCC12(line21_field1, width); + bool cc2 = vbi608->ExtractCC34(line21_field2, width); + if (cc1 || cc2) + { + int code1 = vbi608->GetCode1(); + int code2 = vbi608->GetCode2(); + code1 = (0xFFFF==code1) ? -1 : code1; + code2 = (0xFFFF==code2) ? -1 : code2; + FormatCC(code1, code2); + } + ptr = buf; } else if (ret < 0) { @@ -2790,10 +2803,14 @@ { vbi_del_handler(pal_tt, (void*) vbi_event, &vbicallbackdata); vbi_close(pal_tt); + vbifd = -1; } - if (ntsc_cc) - cc_close(ntsc_cc); + if (vbifd >= 0) + { + close(vbifd); + delete [] buf; + } //VERBOSE(VB_RECORD, LOC + "vbi end"); } Index: libs/libmythtv/avformatdecoder.cpp =================================================================== --- libs/libmythtv/avformatdecoder.cpp (revision 25079) +++ libs/libmythtv/avformatdecoder.cpp (working copy) @@ -22,6 +22,7 @@ #include "mpegtables.h" #include "atscdescriptors.h" #include "dvbdescriptors.h" +#include "vbi608extractor.h" #include "cc608decoder.h" #include "cc708decoder.h" #include "subtitlereader.h" @@ -488,6 +489,8 @@ no_hardware_decoders(no_hardware_decode), maxkeyframedist(-1), // Closed Caption & Teletext decoders + use_vbi(true), + vbi608(new VBI608Extractor()), ccd608(new CC608Decoder(parent->GetCC608Reader())), ccd708(new CC708Decoder(parent->GetCC708Reader())), ttd(new TeletextDecoder()), subReader(parent->GetSubReader()), @@ -552,6 +555,7 @@ } CloseContext(); + delete vbi608; delete ccd608; delete ccd708; delete ttd; @@ -3166,10 +3170,11 @@ // Decode CEA-608 and CEA-708 captions for (uint i = 0; i < (uint)mpa_pic.atsc_cc_len; - i += ((mpa_pic.atsc_cc_buf[i] & 0x1f) * 3) + 2) + i += ((mpa_pic.atsc_cc_buf[i] & 0x1f) * 3) + 2) { DecodeDTVCC(mpa_pic.atsc_cc_buf + i, mpa_pic.atsc_cc_len - i); + use_vbi = false; } VideoFrame *picframe = (VideoFrame *)(mpa_pic.opaque); @@ -3268,6 +3273,13 @@ picframe->repeat_pict = mpa_pic.repeat_pict; picframe->frameNumber = framesPlayed; + + if (use_vbi && vbi608->ExtractCC(picframe)) + { + uint8_t cc_data[8]; + DecodeDTVCC(cc_data, vbi608->FillCCData(cc_data)); + } + GetNVP()->ReleaseNextVideoFrame(picframe, temppts); if (d->HasMPEG2Dec() && mpa_pic.data[3]) context->release_buffer(context, &mpa_pic); Index: libs/libmythtv/libmythtv.pro =================================================================== --- libs/libmythtv/libmythtv.pro (revision 25079) +++ libs/libmythtv/libmythtv.pro (working copy) @@ -147,6 +147,7 @@ HEADERS += remoteencoder.h videosource.h HEADERS += cardutil.h sourceutil.h HEADERS += videometadatautil.h +HEADERS += vbi608extractor.h HEADERS += cc608decoder.h cc608reader.h HEADERS += cc708decoder.h cc708reader.h HEADERS += cc708window.h subtitlereader.h @@ -171,6 +172,7 @@ SOURCES += remoteencoder.cpp videosource.cpp SOURCES += cardutil.cpp sourceutil.cpp SOURCES += videometadatautil.cpp +SOURCES += vbi608extractor.cpp SOURCES += cc608decoder.cpp cc608reader.cpp SOURCES += cc708decoder.cpp cc708reader.cpp SOURCES += cc708window.cpp subtitlereader.cpp Index: libs/libmythtv/NuppelVideoRecorder.h =================================================================== --- libs/libmythtv/NuppelVideoRecorder.h (revision 25079) +++ libs/libmythtv/NuppelVideoRecorder.h (working copy) @@ -37,13 +37,13 @@ struct video_audio; struct VBIData; -struct cc; class RTjpeg; class RingBuffer; class ChannelBase; class FilterManager; class FilterChain; class AudioInput; +class VBI608Extractor; class MPUBLIC NuppelVideoRecorder : public RecorderBase { @@ -138,7 +138,7 @@ void DoMJPEG(void); void FormatTeletextSubtitles(struct VBIData *vbidata); - void FormatCC(struct cc *cc); + void FormatCC(uint code1, uint code2); void AddTextData(unsigned char *buf, int len, long long timecode, char type); @@ -285,6 +285,7 @@ int volume; + VBI608Extractor *vbi608; CC608Decoder *ccd; CC608Reader ccr; Index: libs/libmythtv/avformatdecoder.h =================================================================== --- libs/libmythtv/avformatdecoder.h (revision 25079) +++ libs/libmythtv/avformatdecoder.h (working copy) @@ -22,6 +22,7 @@ #include "avfringbuffer.h" class TeletextDecoder; +class VBI608Extractor; class CC608Decoder; class CC708Decoder; class SubtitleReader; @@ -268,6 +269,9 @@ int maxkeyframedist; // Caption/Subtitle/Teletext decoders + /// How often has 608 data been found in the first lines of video + bool use_vbi; + VBI608Extractor *vbi608; CC608Decoder *ccd608; CC708Decoder *ccd708; TeletextDecoder *ttd; Index: libs/libmythtv/vbi608extractor.cpp =================================================================== --- libs/libmythtv/vbi608extractor.cpp (revision 0) +++ libs/libmythtv/vbi608extractor.cpp (revision 0) @@ -0,0 +1,392 @@ +/* + VBI 608 Extractor, extracts CEA-608 VBI from a line of raw data. + Copyright (C) 2010 Digital Nirvana, Inc. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#define __STDC_LIMIT_MACROS +#include + +#include +using namespace std; + +#include "vbi608extractor.h" +#include "mythverbose.h" + +static void print( + const QList &raw_minimas, const QList &raw_maximas, + const QList &minimas, const QList &maximas) +{ + QString raw_mins, raw_maxs; + for (uint i = 0; i < uint(raw_minimas.size()); i++) + raw_mins += QString("%1,").arg(raw_minimas[i]); + for (uint i = 0; i < uint(raw_maximas.size()); i++) + raw_maxs += QString("%1,").arg(raw_maximas[i]); + VERBOSE(VB_VBI|VB_EXTRA, QString("raw mins: %1").arg(raw_mins)); + VERBOSE(VB_VBI|VB_EXTRA, QString("raw maxs: %1").arg(raw_maxs)); + + QString mins, maxs; + for (uint i = 0; i < uint(minimas.size()); i++) + mins += QString("%1,").arg(minimas[i]); + for (uint i = 0; i < uint(maximas.size()); i++) + maxs += QString("%1,").arg(maximas[i]); + VERBOSE(VB_VBI|VB_EXTRA, QString("mins: %1 maxs: %2") + .arg(mins).arg(maxs)); +} + +static float find_clock_diff(const QList &list) +{ + float min_diff = INT32_MAX; + float max_diff = 0.0f; + float avg_diff = 0.0f; + for (uint i = 1; i < uint(list.size()); i++) + { + float diff = list[i] - list[i-1]; + min_diff = min(diff, min_diff); + max_diff = max(diff, max_diff); + avg_diff += diff; + } + avg_diff /= (list.size() - 1); + if (avg_diff * 1.15 < max_diff) + { + VERBOSE(VB_VBI|VB_EXTRA, "max_diff too big"); + return 0.0f; + } + if (avg_diff * 0.85 > max_diff) + { + VERBOSE(VB_VBI|VB_EXTRA, "min_diff too small"); + return 0.0f; + } + + return avg_diff; +} + +VBI608Extractor::VBI608Extractor() : start(0.0f), rate(0.0f) +{ + code[0] = UINT16_MAX; + code[1] = UINT16_MAX; +} + +bool VBI608Extractor::FindClocks(const unsigned char *buf, uint width) +{ + raw_minimas.clear(); + raw_maximas.clear(); + maximas.clear(); + minimas.clear(); + + // find our threshold + uint maxv = 0; + for (uint j = width / 8; j < width / 4; j++) + maxv = max(uint(buf[j]), maxv); + uint avgv = maxv / 2; + + // get the raw minima and maxima list + uint noise_flr_sm = max(uint(0.003 * width), 2U); + uint noise_flr_lg = max(uint(0.007 * width), noise_flr_sm+1); + int last_max = -1, last_min = -1; + for (uint i = 0; i < (width/3); i++) + { + if (buf[i] > avgv+10) + raw_maximas.push_back(last_max=i); + else if (last_max>=0 && (i - last_max) <= noise_flr_sm) + raw_maximas.push_back(i); + else if (buf[i] < avgv-10) + raw_minimas.push_back(last_min=i); + else if (last_min>=0 && (i - last_min) <= noise_flr_lg) + raw_minimas.push_back(i); + } + + for (uint i = 0; i < uint(raw_maximas.size()); i++) + { + uint start_idx = raw_maximas[i]; + while ((i+1) < uint(raw_maximas.size()) && + (raw_maximas[i+1] == raw_maximas[i] + 1)) i++; + uint end_idx = raw_maximas[i]; + if (end_idx - start_idx > noise_flr_lg) + maximas.push_back((start_idx + end_idx) * 0.5f); + } + + if (maximas.size() < 7) + { + VERBOSE(VB_VBI|VB_EXTRA, QString("%1 < 7").arg(maximas.size())); + print(raw_minimas, raw_maximas, minimas, maximas); + return false; + } + + // drop outliers on edges + bool dropped = true; + while (maximas.size() > 7 && dropped) + { + float min_diff = width * 8; + float max_diff = 0.0f; + float avg_diff = 0.0f; + for (uint i = 1; i < uint(maximas.size()); i++) + { + float diff = maximas[i] - maximas[i-1]; + min_diff = min(diff, min_diff); + max_diff = max(diff, max_diff); + avg_diff += diff; + } + avg_diff -= min_diff; + avg_diff -= max_diff; + avg_diff /= (maximas.size() - 3); + + dropped = false; + if (avg_diff * 1.1f < max_diff) + { + float last_diff = maximas.back() - maximas[maximas.size()-2]; + if (last_diff*1.01f >= max_diff || last_diff > avg_diff * 1.2f) + { + maximas.pop_back(); + dropped = true; + } + float first_diff = maximas[1] - maximas[0]; + if ((maximas.size() > 7) && (first_diff*1.01f >= max_diff)) + { + maximas.pop_front(); + dropped = true; + } + } + + if (avg_diff * 0.9f > min_diff) + { + float last_diff = maximas.back() - maximas[maximas.size()-2]; + if ((maximas.size() > 7) && + (last_diff*0.99f <= min_diff || last_diff < avg_diff * 0.80f)) + { + maximas.pop_back(); + dropped = true; + } + float first_diff = maximas[1] - maximas[0]; + if ((maximas.size() > 7) && (first_diff*0.99f <= min_diff)) + { + maximas.pop_front(); + dropped = true; + } + } + } + + if (maximas.size() != 7) + { + VERBOSE(VB_VBI|VB_EXTRA, QString("maximas: %1 != 7") + .arg(maximas.size())); + print(raw_minimas, raw_maximas, minimas, maximas); + return false; + } + + // find the minimas + for (uint i = 0; i < uint(raw_minimas.size()); i++) + { + uint start_idx = raw_minimas[i]; + while ((i+1) < uint(raw_minimas.size()) && + (raw_minimas[i+1] == raw_minimas[i] + 1)) i++; + uint end_idx = raw_minimas[i]; + float center = (start_idx + end_idx) * 0.5f; + if (end_idx - start_idx > noise_flr_lg && + center > maximas[0] && center < maximas.back()) + { + minimas.push_back(center); + } + } + + if (minimas.size() != 6) + { + VERBOSE(VB_VBI|VB_EXTRA, QString("minimas: %1 != 6") + .arg(minimas.size())); + print(raw_minimas, raw_maximas, minimas, maximas); + return false; + } + + // get the average clock rate in samples + float maxima_avg_diff = find_clock_diff(maximas); + float minima_avg_diff = find_clock_diff(minimas); + rate = (maxima_avg_diff * 7 + minima_avg_diff * 6) / 13.0f; + if (maxima_avg_diff == 0.0f || minima_avg_diff == 0.0f) + return false; + + // get the estimated location of the first maxima + // based on the rate and location of all maximas + start = maximas[0]; + for (uint i = 1; i < uint(maximas.size()); i++) + start += maximas[i] - i * rate; + start /= maximas.size(); + // then move it back by a third to make each sample + // more or less in the center of each encoded byte. + start -= rate * 0.33f; + + // if the last bit is after the last sample... + // 7 clocks + 3 bits run in + 16 bits data + if (start+((7+3+8+8-1) * rate) > width) + { + VERBOSE(VB_VBI|VB_EXTRA, QString("end %1 > width %2") + .arg(start+((7+3+8+8-1) * rate)).arg(width)); + + return false; + } + + //VERBOSE(VB_VBI|VB_EXTRA, QString("Clock start %1, rate %2") + // .arg(start).arg(avg_diff)); + + return true; +} + +bool VBI608Extractor::ExtractCC(const VideoFrame *picframe, uint max_lines) +{ + int ypitch = picframe->pitches[0]; + int ywidth = picframe->width; + + code[0] = UINT16_MAX; + code[1] = UINT16_MAX; + + // find CC + uint found_cnt = 0; + for (uint i = 0; i < max_lines; i++) + { + const unsigned char *y = picframe->buf + + picframe->offsets[0] + (i * ypitch); + if (FindClocks(y, ywidth)) + { + uint maxv = 0; + for (uint j = 0; j < start + 8 * rate; j++) + maxv = max(uint((y+(i * ypitch))[j]), maxv); + uint avgv = maxv / 2; + + if (y[uint(start + (0+7) * rate)] > avgv || + y[uint(start + (1+7) * rate)] > avgv || + y[uint(start + (2+7) * rate)] < avgv) + { + continue; // need 001 at run in.. + } + + code[found_cnt] = 0; + for (uint j = 0; j < 8+8; j++) + { + bool bit = y[uint(start + (j+7+3) * rate)] > avgv; + code[found_cnt] = + (code[found_cnt]>>1) | (bit?(1<<15):0); + } + + found_cnt++; + if (found_cnt>=2) + break; +#if 0 + unsigned char *Y = const_cast(y); + unsigned char *u = const_cast + (picframe->buf + picframe->offsets[1] + + (i*picframe->pitches[1])); + unsigned char *v = const_cast + (picframe->buf + picframe->offsets[2] + + (i*picframe->pitches[2])); + unsigned uwidth = picframe->pitches[1]; + v[uwidth / 3] = 0x40; + for (uint j = 0; j < 7+3+8+8; j++) + { + uint yloc = uint (start + j * rate + 0.5); + Y[yloc] = 0xFF; + uint uloc = uint (uwidth * (start + j * rate + 0.5) / ywidth); + u[uloc] = 0x40; + } +#endif + } + } + + return found_cnt; +} + +bool VBI608Extractor::ExtractCC12(const unsigned char *buf, uint width) +{ + code[0] = UINT16_MAX; + if (FindClocks(buf, width)) + { + uint maxv = 0; + for (uint j = 0; j < start + 8 * rate; j++) + maxv = max(uint(buf[j]), maxv); + uint avgv = maxv / 2; + + if (buf[uint(start + (0+7) * rate)] > avgv || + buf[uint(start + (1+7) * rate)] > avgv || + buf[uint(start + (2+7) * rate)] < avgv) + { + return false; + } + + code[0] = 0; + for (uint j = 0; j < 8+8; j++) + { + bool bit = buf[uint(start + (j+7+3) * rate)] > avgv; + code[0] = (code[0]>>1) | (bit?(1<<15):0); + } + + return true; + } + return false; +} + +bool VBI608Extractor::ExtractCC34(const unsigned char *buf, uint width) +{ + code[1] = UINT16_MAX; + if (FindClocks(buf, width)) + { + uint maxv = 0; + for (uint j = 0; j < start + 8 * rate; j++) + maxv = max(uint(buf[j]), maxv); + uint avgv = maxv / 2; + + if (buf[uint(start + (0+7) * rate)] > avgv || + buf[uint(start + (1+7) * rate)] > avgv || + buf[uint(start + (2+7) * rate)] < avgv) + { + return false; + } + + code[1] = 0; + for (uint j = 0; j < 8+8; j++) + { + bool bit = buf[uint(start + (j+7+3) * rate)] > avgv; + code[1] = (code[1]>>1) | (bit?(1<<15):0); + } + return true; + } + return false; +} + +uint VBI608Extractor::FillCCData(uint8_t cc_data[8]) +{ + uint cc_count = 0; + if (code[0] != UINT16_MAX) + { + cc_data[2] = 0x04; + cc_data[3] = (code[0]) & 0xff; + cc_data[4] = (code[0]>>8) & 0xff; + cc_count++; + } + + if (code[1] != UINT16_MAX) + { + cc_data[2+3*cc_count] = 0x04|0x01; + cc_data[3+3*cc_count] = (code[1]) & 0xff; + cc_data[4+3*cc_count] = (code[1]>>8) & 0xff; + cc_count++; + } + + if (cc_count) + { + cc_data[0] = 0x40 | cc_count; + cc_data[1] = 0x00; + return 2+(3*cc_count); + } + return 0; +}