Skip to content

Commit

Permalink
Merge pull request #777 from atsampson/splitchroma
Browse files Browse the repository at this point in the history
Allow separate chroma output from ld-chroma-encoder
  • Loading branch information
happycube authored Aug 22, 2022
2 parents 0e0a3d9 + 288c3d1 commit 0d08975
Show file tree
Hide file tree
Showing 8 changed files with 405 additions and 375 deletions.
192 changes: 192 additions & 0 deletions tools/ld-chroma-decoder/encoder/encoder.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/************************************************************************
encoder.cpp
ld-chroma-encoder - Composite video encoder
Copyright (C) 2019-2022 Adam Sampson
Copyright (C) 2022 Phillip Blucas
This file is part of ld-decode-tools.
ld-chroma-encoder 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 3 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, see <http://www.gnu.org/licenses/>.
************************************************************************/

/*!
\class Encoder
This is the abstract base class for video encoders. A subclass of Encoder
implements an encoder for a particular video standard.
References:
[Poynton] "Digital Video and HDTV Algorithms and Interfaces" by Charles
Poynton, 2003, first edition, ISBN 1-55860-792-7. Later editions have less
material about analogue video standards.
[EBU] "Specification of interfaces for 625-line digital PAL signals",
(https://tech.ebu.ch/docs/tech/tech3280.pdf) EBU Tech. 3280-E.
[SMPTE] "System M/NTSC Composite Video Signals Bit-Parallel Digital Interface",
(https://ieeexplore.ieee.org/document/7290873) SMPTE 244M-2003.
[Clarke] "Colour encoding and decoding techniques for line-locked sampled
PAL and NTSC television signals" (https://www.bbc.co.uk/rd/publications/rdreport_1986_02),
BBC Research Department Report 1986/02, by C.K.P. Clarke.
*/

#include "encoder.h"

Encoder::Encoder(QFile &_rgbFile, QFile &_tbcFile, QFile &_chromaFile, LdDecodeMetaData &_metaData)
: rgbFile(_rgbFile), tbcFile(_tbcFile), chromaFile(_chromaFile), metaData(_metaData)
{
}

bool Encoder::encode()
{
// Store video parameters
metaData.setVideoParameters(videoParameters);

// Process frames until EOF
qint32 numFrames = 0;
while (true) {
qint32 result = encodeFrame(numFrames);
if (result == -1) {
return false;
} else if (result == 0) {
break;
}
numFrames++;
}

return true;
}

// Read one frame from the input, and write two fields to the output.
// Returns 0 on EOF, 1 on success; on failure, prints an error and returns -1.
qint32 Encoder::encodeFrame(qint32 frameNo)
{
// Read the input frame
qint64 remainBytes = rgbFrame.size();
qint64 posBytes = 0;
while (remainBytes > 0) {
qint64 count = rgbFile.read(rgbFrame.data() + posBytes, remainBytes);
if (count == 0 && remainBytes == rgbFrame.size()) {
// EOF at the start of a frame
return 0;
} else if (count == 0) {
qCritical() << "Unexpected end of input file";
return -1;
} else if (count < 0) {
qCritical() << "Error reading from input file";
return -1;
}
remainBytes -= count;
posBytes += count;
}

// Write the two fields -- even-numbered lines, then odd-numbered lines.
// In a TBC file, the first field is always the one that starts with the
// half-line (i.e. frame line 44 for PAL or 39 for NTSC, counting from 0).
if (!encodeField(frameNo * 2)) {
return -1;
}
if (!encodeField((frameNo * 2) + 1)) {
return -1;
}

return 1;
}

// Encode one field from rgbFrame to the output.
// Returns true on success; on failure, prints an error and returns false.
bool Encoder::encodeField(qint32 fieldNo)
{
const qint32 lineOffset = fieldNo % 2;

// Output from the encoder
std::vector<double> outputC(videoParameters.fieldWidth);
std::vector<double> outputVBS(videoParameters.fieldWidth);

// Buffer for conversion
std::vector<quint16> outputBuffer(videoParameters.fieldWidth);

for (qint32 frameLine = 0; frameLine < 2 * videoParameters.fieldHeight; frameLine++) {
// Skip lines that aren't in this field
if ((frameLine % 2) != lineOffset) {
continue;
}

// Encode the line
const quint16 *rgbData = nullptr;
if (frameLine >= activeTop && frameLine < (activeTop + activeHeight)) {
rgbData = reinterpret_cast<const quint16 *>(rgbFrame.data()) + ((frameLine - activeTop) * activeWidth * 3);
}
encodeLine(fieldNo, frameLine, rgbData, outputC, outputVBS);

if (chromaFile.isOpen()) {
// Write C and VBS to separate output files
if (!writeLine(outputC, outputBuffer, true, chromaFile)) return false;
if (!writeLine(outputVBS, outputBuffer, false, tbcFile)) return false;
} else {
// Combine C and VBS into a single output file
for (qint32 x = 0; x < videoParameters.fieldWidth; x++) {
outputVBS[x] += outputC[x];
}
if (!writeLine(outputVBS, outputBuffer, false, tbcFile)) return false;
}
}

// Generate field metadata
LdDecodeMetaData::Field fieldData;
getFieldMetadata(fieldNo, fieldData);
metaData.appendField(fieldData);

return true;
}

bool Encoder::writeLine(const std::vector<double> &input, std::vector<quint16> &buffer, bool isChroma, QFile &file)
{
// Scale to a 16-bit output sample and limit the excursion to the
// permitted sample values. [EBU p6] [SMPTE p6]
//
// With PAL line-locked sampling, some colours (e.g. the yellow
// colourbar) can result in values outside this range because there
// isn't enough headroom.
//
// Separate chroma is scaled like the normal signal, but centred on 0x7FFF.
const double scale = videoParameters.white16bIre - videoParameters.black16bIre;
const double offset = isChroma ? 0x7FFF : videoParameters.black16bIre;
for (qint32 x = 0; x < videoParameters.fieldWidth; x++) {
const double scaled = qBound(static_cast<double>(0x0100), (input[x] * scale) + offset, static_cast<double>(0xFEFF));
buffer[x] = static_cast<quint16>(scaled);
}

// Write the converted line to the output file.
// TBC data is unsigned 16-bit values in native byte order.
const char *outputData = reinterpret_cast<const char *>(buffer.data());
qint64 remainBytes = buffer.size() * 2;
qint64 posBytes = 0;
while (remainBytes > 0) {
qint64 count = file.write(outputData + posBytes, remainBytes);
if (count < 0) {
qCritical() << "Error writing to output file";
return false;
}
remainBytes -= count;
posBytes += count;
}

return true;
}
96 changes: 96 additions & 0 deletions tools/ld-chroma-decoder/encoder/encoder.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/************************************************************************
encoder.h
ld-chroma-encoder - Composite video encoder
Copyright (C) 2019-2022 Adam Sampson
Copyright (C) 2022 Phillip Blucas
This file is part of ld-decode-tools.
ld-chroma-encoder 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 3 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, see <http://www.gnu.org/licenses/>.
************************************************************************/

#ifndef ENCODER_H
#define ENCODER_H

#include <QByteArray>
#include <QFile>
#include <cmath>
#include <vector>

#include "lddecodemetadata.h"

class Encoder
{
public:
// Constructor.
// This only sets the member variables it takes as parameters; subclasses
// must initialise the VideoParameters, compute the active region and
// resize rgbFrame.
Encoder(QFile &rgbFile, QFile &tbcFile, QFile &chromaFile, LdDecodeMetaData &metaData);

// Encode RGB stream to TBC.
// Returns true on success; on failure, prints an error and returns false.
bool encode();

protected:
qint32 encodeFrame(qint32 frameNo);
bool encodeField(qint32 fieldNo);

// Fill in the metadata for a generated field
virtual void getFieldMetadata(qint32 fieldNo, LdDecodeMetaData::Field &fieldData) = 0;

// Encode one line of a field into composite video.
// outputC includes the chroma signal and burst.
// outputVBS includes the luma signal, blanking and syncs.
virtual void encodeLine(qint32 fieldNo, qint32 frameLine, const quint16 *rgbData,
std::vector<double> &outputC, std::vector<double> &outputVBS) = 0;

// Scale and write a line of data to one of the output files.
// Returns true on success; on failure, prints an error and returns false.
bool writeLine(const std::vector<double> &input, std::vector<quint16> &buffer, bool isChroma, QFile &file);

QFile &rgbFile;
QFile &tbcFile;
QFile &chromaFile;
LdDecodeMetaData &metaData;

LdDecodeMetaData::VideoParameters videoParameters;
qint32 activeWidth;
qint32 activeHeight;
qint32 activeLeft;
qint32 activeTop;

QByteArray rgbFrame;
};

// Generate a gate waveform with raised-cosine transitions, with 50% points at given start and end times
static inline double raisedCosineGate(double t, double startTime, double endTime, double halfRiseTime)
{
if (t < startTime - halfRiseTime) {
return 0.0;
} else if (t < startTime + halfRiseTime) {
return 0.5 + (0.5 * sin((M_PI / 2.0) * ((t - startTime) / halfRiseTime)));
} else if (t < endTime - halfRiseTime) {
return 1.0;
} else if (t < endTime + halfRiseTime) {
return 0.5 - (0.5 * sin((M_PI / 2.0) * ((t - endTime) / halfRiseTime)));
} else {
return 0.0;
}
}

#endif
2 changes: 2 additions & 0 deletions tools/ld-chroma-decoder/encoder/encoder.pro
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ win32:DEFINES += _USE_MATH_DEFINES
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0

SOURCES += \
encoder.cpp \
main.cpp \
ntscencoder.cpp \
palencoder.cpp \
Expand All @@ -28,6 +29,7 @@ SOURCES += \
../../library/tbc/vbidecoder.cpp

HEADERS += \
encoder.h \
ntscencoder.h \
palencoder.h \
../../library/filter/firfilter.h \
Expand Down
35 changes: 25 additions & 10 deletions tools/ld-chroma-decoder/encoder/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
main.cpp
ld-chroma-encoder - PAL encoder for testing
Copyright (C) 2019-2020 Adam Sampson
ld-chroma-encoder - Composite video encoder
Copyright (C) 2019-2022 Adam Sampson
This file is part of ld-decode-tools.
ld-chroma-decoder is free software: you can redistribute it and/or
ld-chroma-encoder 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 3 of the
License, or (at your option) any later version.
Expand Down Expand Up @@ -51,9 +51,9 @@ int main(int argc, char *argv[])
// Set up the command line parser
QCommandLineParser parser;
parser.setApplicationDescription(
"ld-chroma-encoder - PAL/NTSC encoder for testing\n"
"ld-chroma-encoder - Composite video encoder\n"
"\n"
"(c)2019-2020 Adam Sampson\n"
"(c)2019-2022 Adam Sampson\n"
"(c)2022 Phillip Blucas\n"
"GPLv3 Open-Source - github: https://github.com/happycube/ld-decode");
parser.addHelpOption();
Expand Down Expand Up @@ -94,6 +94,10 @@ int main(int argc, char *argv[])
// Positional argument to specify output video file
parser.addPositionalArgument("output", QCoreApplication::translate("main", "Specify output TBC file"));

// Positional argument to specify chroma output video file
parser.addPositionalArgument("chroma", QCoreApplication::translate("main", "Specify chroma output TBC file (optional)"),
"[chroma]");

// Process the command line options and arguments given by the user
parser.process(a);

Expand All @@ -106,10 +110,14 @@ int main(int argc, char *argv[])
// Get the arguments from the parser
QString inputFileName;
QString outputFileName;
QString chromaFileName;
QStringList positionalArguments = parser.positionalArguments();
if (positionalArguments.count() == 2) {
if (positionalArguments.count() == 2 || positionalArguments.count() == 3) {
inputFileName = positionalArguments.at(0);
outputFileName = positionalArguments.at(1);
if (positionalArguments.count() > 2) {
chromaFileName = positionalArguments.at(2);
}
} else {
// Quit with error
qCritical("You must specify the input RGB and output TBC files");
Expand Down Expand Up @@ -167,22 +175,29 @@ int main(int argc, char *argv[])
}
}

// Open the output file
// Open the main output file
QFile tbcFile(outputFileName);
if (!tbcFile.open(QFile::WriteOnly)) {
qCritical() << "Cannot open output file:" << outputFileName;
return -1;
}

// Open the chroma output file, if specified
QFile chromaFile(chromaFileName);
if (chromaFileName != "" && !chromaFile.open(QFile::WriteOnly)) {
qCritical() << "Cannot open chroma output file:" << chromaFileName;
return -1;
}

// Encode the data
LdDecodeMetaData metaData;
if( system == NTSC ) {
NTSCEncoder encoder(rgbFile, tbcFile, metaData, chromaMode, addSetup);
if (system == NTSC) {
NTSCEncoder encoder(rgbFile, tbcFile, chromaFile, metaData, chromaMode, addSetup);
if (!encoder.encode()) {
return -1;
}
} else {
PALEncoder encoder(rgbFile, tbcFile, metaData, scLocked);
PALEncoder encoder(rgbFile, tbcFile, chromaFile, metaData, scLocked);
if (!encoder.encode()) {
return -1;
}
Expand Down
Loading

0 comments on commit 0d08975

Please sign in to comment.