Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow separate chroma output from ld-chroma-encoder #777

Merged
merged 6 commits into from
Aug 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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