mirror of https://github.com/f4exb/sdrangel.git
ATV Demodulator: improvements of HSync and VSync algorithms. Fixes #459.
This commit is contained in:
parent
24ca0f0d56
commit
4afd5a7a2d
|
@ -243,6 +243,7 @@ ATVDemodGUI::ATVDemodGUI(PluginAPI* objPluginAPI, DeviceUISet *deviceUISet, Base
|
||||||
{
|
{
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
ui->screenTV->setColor(false);
|
ui->screenTV->setColor(false);
|
||||||
|
ui->screenTV->setExtraColumns(true);
|
||||||
setAttribute(Qt::WA_DeleteOnClose, true);
|
setAttribute(Qt::WA_DeleteOnClose, true);
|
||||||
connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool)));
|
connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool)));
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ void ATVDemodSettings::resetToDefaults()
|
||||||
m_vSync = false;
|
m_vSync = false;
|
||||||
m_invertVideo = false;
|
m_invertVideo = false;
|
||||||
m_halfFrames = false; // m_fltRatioOfRowsToDisplay = 1.0
|
m_halfFrames = false; // m_fltRatioOfRowsToDisplay = 1.0
|
||||||
m_levelSynchroTop = 0.1f;
|
m_levelSynchroTop = 0.15f;
|
||||||
m_levelBlack = 0.3f;
|
m_levelBlack = 0.3f;
|
||||||
m_lineTimeFactor = 0;
|
m_lineTimeFactor = 0;
|
||||||
m_topTimeFactor = 25;
|
m_topTimeFactor = 25;
|
||||||
|
|
|
@ -36,11 +36,8 @@ ATVDemodSink::ATVDemodSink() :
|
||||||
m_scopeSink(nullptr),
|
m_scopeSink(nullptr),
|
||||||
m_registeredTVScreen(nullptr),
|
m_registeredTVScreen(nullptr),
|
||||||
m_numberSamplesPerHTop(0),
|
m_numberSamplesPerHTop(0),
|
||||||
m_imageIndex(0),
|
m_fieldIndex(0),
|
||||||
m_synchroSamples(0),
|
m_synchroSamples(0),
|
||||||
m_verticalSynchroDetected(false),
|
|
||||||
m_ampLineSum(0.0f),
|
|
||||||
m_ampLineAvg(0.0f),
|
|
||||||
m_effMin(20.0f),
|
m_effMin(20.0f),
|
||||||
m_effMax(-20.0f),
|
m_effMax(-20.0f),
|
||||||
m_ampMin(-1.0f),
|
m_ampMin(-1.0f),
|
||||||
|
@ -48,10 +45,13 @@ ATVDemodSink::ATVDemodSink() :
|
||||||
m_ampDelta(2.0f),
|
m_ampDelta(2.0f),
|
||||||
m_colIndex(0),
|
m_colIndex(0),
|
||||||
m_sampleIndex(0),
|
m_sampleIndex(0),
|
||||||
|
m_sampleIndexDetected(0),
|
||||||
|
m_hSyncShiftSum(0.0f),
|
||||||
|
m_hSyncShiftCount(0),
|
||||||
|
m_hSyncErrorCount(0),
|
||||||
m_amSampleIndex(0),
|
m_amSampleIndex(0),
|
||||||
m_rowIndex(0),
|
m_rowIndex(0),
|
||||||
m_lineIndex(0),
|
m_lineIndex(0),
|
||||||
m_objAvgColIndex(3),
|
|
||||||
m_ampAverage(4800),
|
m_ampAverage(4800),
|
||||||
m_bfoPLL(200/1000000, 100/1000000, 0.01),
|
m_bfoPLL(200/1000000, 100/1000000, 0.01),
|
||||||
m_bfoFilter(200.0, 1000000.0, 0.9),
|
m_bfoFilter(200.0, 1000000.0, 0.9),
|
||||||
|
@ -66,8 +66,6 @@ ATVDemodSink::ATVDemodSink() :
|
||||||
//m_intNumberSamplePerLine=0;
|
//m_intNumberSamplePerLine=0;
|
||||||
m_synchroSamples=0;
|
m_synchroSamples=0;
|
||||||
m_interleaved = true;
|
m_interleaved = true;
|
||||||
m_firstRowIndexEven = 0;
|
|
||||||
m_firstRowIndexOdd = 0;
|
|
||||||
|
|
||||||
m_DSBFilter = new fftfilt(m_settings.m_fftBandwidth / (float) m_tvSampleRate, 2*m_ssbFftLen); // arbitrary cutoff
|
m_DSBFilter = new fftfilt(m_settings.m_fftBandwidth / (float) m_tvSampleRate, 2*m_ssbFftLen); // arbitrary cutoff
|
||||||
m_DSBFilterBuffer = new Complex[m_ssbFftLen];
|
m_DSBFilterBuffer = new Complex[m_ssbFftLen];
|
||||||
|
@ -348,70 +346,80 @@ void ATVDemodSink::applyStandard(int sampleRate, const ATVDemodSettings& setting
|
||||||
{
|
{
|
||||||
case ATVDemodSettings::ATVStdHSkip:
|
case ATVDemodSettings::ATVStdHSkip:
|
||||||
// what is left in a line for the image
|
// what is left in a line for the image
|
||||||
m_numberOfSyncLines = 0;
|
|
||||||
m_numberOfBlackLines = 0;
|
|
||||||
m_numberOfEqLines = 0; // not applicable
|
|
||||||
m_numberSamplesHSyncCrop = (int) (0.09f * lineDuration * sampleRate); // 9% of full line empirically
|
|
||||||
m_interleaved = false; // irrelevant
|
m_interleaved = false; // irrelevant
|
||||||
m_firstRowIndexEven = 0; // irrelevant
|
m_numberOfBlackLines = 0;
|
||||||
m_firstRowIndexOdd = 0; // irrelevant
|
m_numberSamplesHSyncCrop = (int) (0.09f * lineDuration * sampleRate); // 9% of full line empirically
|
||||||
break;
|
break;
|
||||||
case ATVDemodSettings::ATVStdShort:
|
case ATVDemodSettings::ATVStdShort:
|
||||||
// what is left in a line for the image
|
// what is left in a line for the image
|
||||||
m_numberOfSyncLines = 4;
|
|
||||||
m_numberOfBlackLines = 5;
|
|
||||||
m_numberOfEqLines = 0;
|
|
||||||
m_numberSamplesHSyncCrop = (int) (0.085f * lineDuration * sampleRate); // 8.5% of full line empirically
|
|
||||||
m_interleaved = false;
|
m_interleaved = false;
|
||||||
m_firstRowIndexEven = 0; // irrelevant
|
m_numberOfVSyncLines = 2;
|
||||||
m_firstRowIndexOdd = 0; // irrelevant
|
m_numberOfBlackLines = 4;
|
||||||
|
m_firstVisibleLine = 3;
|
||||||
|
m_numberSamplesHSyncCrop = (int) (0.085f * lineDuration * sampleRate); // 8.5% of full line empirically
|
||||||
break;
|
break;
|
||||||
case ATVDemodSettings::ATVStdShortInterleaved:
|
case ATVDemodSettings::ATVStdShortInterleaved:
|
||||||
// what is left in a line for the image
|
// what is left in a line for the image
|
||||||
m_numberOfSyncLines = 4;
|
|
||||||
m_numberOfBlackLines = 7;
|
|
||||||
m_numberOfEqLines = 0;
|
|
||||||
m_numberSamplesHSyncCrop = (int) (0.085f * lineDuration * sampleRate); // 8.5% of full line empirically
|
|
||||||
m_interleaved = true;
|
m_interleaved = true;
|
||||||
m_firstRowIndexEven = 0;
|
m_numberOfVSyncLines = 2;
|
||||||
m_firstRowIndexOdd = 1;
|
m_numberOfBlackLines = 5;
|
||||||
|
m_firstVisibleLine = 3;
|
||||||
|
m_numberSamplesHSyncCrop = (int) (0.085f * lineDuration * sampleRate); // 8.5% of full line empirically
|
||||||
break;
|
break;
|
||||||
case ATVDemodSettings::ATVStd405: // Follows loosely the 405 lines standard
|
case ATVDemodSettings::ATVStd405: // Follows loosely the 405 lines standard
|
||||||
// what is left in a ine for the image
|
// what is left in a line for the image
|
||||||
m_numberOfSyncLines = 24; // (15+7)*2 - 20
|
|
||||||
m_numberOfBlackLines = 30; // above + 6
|
|
||||||
m_numberOfEqLines = 3;
|
|
||||||
m_numberSamplesHSyncCrop = (int) (0.085f * lineDuration * sampleRate); // 8.5% of full line empirically
|
|
||||||
m_interleaved = true;
|
m_interleaved = true;
|
||||||
m_firstRowIndexEven = 0;
|
m_numberOfVSyncLines = 3;
|
||||||
m_firstRowIndexOdd = 3;
|
m_numberOfBlackLines = 30;
|
||||||
|
m_firstVisibleLine = 13;
|
||||||
|
m_numberSamplesHSyncCrop = (int) (0.085f * lineDuration * sampleRate); // 8.5% of full line empirically
|
||||||
break;
|
break;
|
||||||
case ATVDemodSettings::ATVStdPAL525: // Follows PAL-M standard
|
case ATVDemodSettings::ATVStdPAL525: // Follows PAL-M standard
|
||||||
// what is left in a 64/1.008 us line for the image
|
// what is left in a 64/1.008 us line for the image
|
||||||
m_numberOfSyncLines = 40; // (15+15)*2 - 20
|
|
||||||
m_numberOfBlackLines = 46; // above + 6
|
|
||||||
m_numberOfEqLines = 3;
|
|
||||||
m_numberSamplesHSyncCrop = (int) (0.085f * lineDuration * sampleRate); // 8.5% of full line empirically
|
|
||||||
m_interleaved = true;
|
m_interleaved = true;
|
||||||
m_firstRowIndexEven = 0;
|
m_numberOfVSyncLines = 4;
|
||||||
m_firstRowIndexOdd = 3;
|
m_numberOfBlackLines = 45;
|
||||||
|
m_firstVisibleLine = 20;
|
||||||
|
m_numberSamplesHSyncCrop = (int) (0.085f * lineDuration * sampleRate); // 8.5% of full line empirically
|
||||||
break;
|
break;
|
||||||
case ATVDemodSettings::ATVStdPAL625: // Follows PAL-B/G/H standard
|
case ATVDemodSettings::ATVStdPAL625: // Follows PAL-B/G/H standard
|
||||||
default:
|
default:
|
||||||
// what is left in a 64 us line for the image
|
// what is left in a 64 us line for the image
|
||||||
m_numberOfSyncLines = 44; // (15+17)*2 - 20
|
|
||||||
m_numberOfBlackLines = 50; // above + 6
|
|
||||||
m_numberOfEqLines = 3;
|
|
||||||
m_numberSamplesHSyncCrop = (int) (0.085f * lineDuration * sampleRate); // 8.5% of full line empirically
|
|
||||||
m_interleaved = true;
|
m_interleaved = true;
|
||||||
m_firstRowIndexEven = 0;
|
m_numberOfVSyncLines = 3;
|
||||||
m_firstRowIndexOdd = 3;
|
m_numberOfBlackLines = 49;
|
||||||
|
m_firstVisibleLine = 23;
|
||||||
|
m_numberSamplesHSyncCrop = (int) (0.085f * lineDuration * sampleRate); // 8.5% of full line empirically
|
||||||
}
|
}
|
||||||
|
|
||||||
// for now all standards apply this
|
// for now all standards apply this
|
||||||
m_numberSamplesPerLineSignals = (int) ((12.0f/64.0f) * lineDuration * sampleRate); // 12.0 = 2.6 + 4.7 + 4.7 : front porch + horizontal sync pulse + back porch
|
|
||||||
m_numberSamplesPerHSync = (int) ((9.6f/64.0f) * lineDuration * sampleRate); // 9.4 = 4.7 + 4.7 : horizontal sync pulse + back porch
|
// Rec. ITU-R BT.1700
|
||||||
m_numberSamplesPerHTopNom = (int) ((4.7f/64.0f) * lineDuration * sampleRate); // 4.7 : horizontal sync pulse (ultra black) nominal value
|
// Table 2. Details of line synchronizing signals
|
||||||
|
m_numberSamplesPerLineSignals = (int)(lineDuration * sampleRate * 12.0 / 64.0); // "a", Line-blanking interval
|
||||||
|
m_numberSamplesPerHSync = (int)(lineDuration * sampleRate * 10.5 / 64.0); // "b", Interval between time datum and back edge of line-blanking pulse
|
||||||
|
m_numberSamplesPerHTopNom = (int)(lineDuration * sampleRate * 4.7 / 64.0); // "d", Duration of synchronizing pulse
|
||||||
|
|
||||||
|
// Table 3. Details of field synchronizing signals
|
||||||
|
float hl = 32.0f; // half of the line
|
||||||
|
float p = 2.35f; // "p", Duration of equalizing pulse
|
||||||
|
float q = 27.3f; // "q", Duration of field-synchronizing pulse
|
||||||
|
|
||||||
|
// In the first half of the first line field index is detected
|
||||||
|
m_fieldDetectStartPos = (int)(lineDuration * sampleRate * p / 64.0);
|
||||||
|
m_fieldDetectEndPos = (int)(lineDuration * sampleRate * q / 64.0);
|
||||||
|
// In the second half of the first line vertical synchronization is detected
|
||||||
|
m_vSyncDetectStartPos = (int)(lineDuration * sampleRate * (p + hl) / 64.0);
|
||||||
|
m_vSyncDetectEndPos = (int)(lineDuration * sampleRate * (q + hl) / 64.0);
|
||||||
|
|
||||||
|
float fieldDetectPercent = 0.75f; // It is better not to detect field index than detect it wrong
|
||||||
|
float detectTotalLen = lineDuration * sampleRate * (q - p) / 64.0; // same for field index and vSync detection
|
||||||
|
m_fieldDetectThreshold1 = (int)(detectTotalLen * fieldDetectPercent);
|
||||||
|
m_fieldDetectThreshold2 = (int)(detectTotalLen * (1.0f - fieldDetectPercent));
|
||||||
|
|
||||||
|
float vSyncDetectPercent = 0.5f;
|
||||||
|
m_vSyncDetectThreshold = (int)(detectTotalLen * vSyncDetectPercent);
|
||||||
|
|
||||||
m_numberSamplesPerHTop = m_numberSamplesPerHTopNom * (settings.m_topTimeFactor / 100.0f); // adjust the value used in the system
|
m_numberSamplesPerHTop = m_numberSamplesPerHTopNom * (settings.m_topTimeFactor / 100.0f); // adjust the value used in the system
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -444,8 +452,9 @@ void ATVDemodSink::applyChannelSettings(int channelSampleRate, int channelFreque
|
||||||
|
|
||||||
if ((channelSampleRate != m_channelSampleRate) || force)
|
if ((channelSampleRate != m_channelSampleRate) || force)
|
||||||
{
|
{
|
||||||
ATVDemodSettings::getBaseValues(channelSampleRate, m_settings.m_nbLines * m_settings.m_fps, m_tvSampleRate, m_samplesPerLineNom);
|
unsigned int samplesPerLineNom;
|
||||||
m_samplesPerLine = m_samplesPerLineNom + m_settings.m_lineTimeFactor;
|
ATVDemodSettings::getBaseValues(channelSampleRate, m_settings.m_nbLines * m_settings.m_fps, m_tvSampleRate, samplesPerLineNom);
|
||||||
|
m_samplesPerLine = samplesPerLineNom + m_settings.m_lineTimeFactor;
|
||||||
qDebug() << "ATVDemodSink::applyChannelSettings:"
|
qDebug() << "ATVDemodSink::applyChannelSettings:"
|
||||||
<< " m_tvSampleRate: " << m_tvSampleRate
|
<< " m_tvSampleRate: " << m_tvSampleRate
|
||||||
<< " m_fftBandwidth: " << m_settings.m_fftBandwidth
|
<< " m_fftBandwidth: " << m_settings.m_fftBandwidth
|
||||||
|
@ -490,7 +499,7 @@ void ATVDemodSink::applyChannelSettings(int channelSampleRate, int channelFreque
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_imageIndex = 0;
|
m_fieldIndex = 0;
|
||||||
m_colIndex = 0;
|
m_colIndex = 0;
|
||||||
m_rowIndex = 0;
|
m_rowIndex = 0;
|
||||||
|
|
||||||
|
@ -535,9 +544,10 @@ void ATVDemodSink::applySettings(const ATVDemodSettings& settings, bool force)
|
||||||
|| (settings.m_atvStd != m_settings.m_atvStd)
|
|| (settings.m_atvStd != m_settings.m_atvStd)
|
||||||
|| (settings.m_lineTimeFactor != m_settings.m_lineTimeFactor) || force)
|
|| (settings.m_lineTimeFactor != m_settings.m_lineTimeFactor) || force)
|
||||||
{
|
{
|
||||||
ATVDemodSettings::getBaseValues(m_channelSampleRate, settings.m_nbLines * settings.m_fps, m_tvSampleRate, m_samplesPerLineNom);
|
unsigned int samplesPerLineNom;
|
||||||
m_samplesPerLine = m_samplesPerLineNom + settings.m_lineTimeFactor;
|
ATVDemodSettings::getBaseValues(m_channelSampleRate, settings.m_nbLines * settings.m_fps, m_tvSampleRate, samplesPerLineNom);
|
||||||
m_ampAverage.resize(m_samplesPerLine * m_settings.m_nbLines * settings.m_fps * 2); // AGC average in two full images
|
m_samplesPerLine = samplesPerLineNom + settings.m_lineTimeFactor;
|
||||||
|
m_ampAverage.resize(m_samplesPerLine * m_settings.m_nbLines * 2); // AGC average in two full images
|
||||||
|
|
||||||
qDebug() << "ATVDemodSink::applySettings:"
|
qDebug() << "ATVDemodSink::applySettings:"
|
||||||
<< " m_tvSampleRate: " << m_tvSampleRate
|
<< " m_tvSampleRate: " << m_tvSampleRate
|
||||||
|
@ -582,7 +592,7 @@ void ATVDemodSink::applySettings(const ATVDemodSettings& settings, bool force)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_imageIndex = 0;
|
m_fieldIndex = 0;
|
||||||
m_colIndex = 0;
|
m_colIndex = 0;
|
||||||
m_rowIndex = 0;
|
m_rowIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,8 +97,7 @@ private:
|
||||||
int m_channelSampleRate;
|
int m_channelSampleRate;
|
||||||
int m_channelFrequencyOffset;
|
int m_channelFrequencyOffset;
|
||||||
int m_tvSampleRate;
|
int m_tvSampleRate;
|
||||||
unsigned int m_samplesPerLineNom; //!< number of samples per complete line (includes sync signals) - nominal value
|
int m_samplesPerLine; //!< number of samples per complete line (includes sync signals) - adusted value
|
||||||
unsigned int m_samplesPerLine; //!< number of samples per complete line (includes sync signals) - adusted value
|
|
||||||
ATVDemodSettings m_settings;
|
ATVDemodSettings m_settings;
|
||||||
int m_videoTabIndex;
|
int m_videoTabIndex;
|
||||||
|
|
||||||
|
@ -113,25 +112,31 @@ private:
|
||||||
//int m_intNumberSamplePerLine;
|
//int m_intNumberSamplePerLine;
|
||||||
int m_numberSamplesPerHTopNom; //!< number of samples per horizontal synchronization pulse (pulse in ultra-black) - nominal value
|
int m_numberSamplesPerHTopNom; //!< number of samples per horizontal synchronization pulse (pulse in ultra-black) - nominal value
|
||||||
int m_numberSamplesPerHTop; //!< number of samples per horizontal synchronization pulse (pulse in ultra-black) - adusted value
|
int m_numberSamplesPerHTop; //!< number of samples per horizontal synchronization pulse (pulse in ultra-black) - adusted value
|
||||||
int m_numberOfSyncLines; //!< this is the number of non displayable lines at the start of a frame. First displayable row comes next.
|
|
||||||
int m_numberOfBlackLines; //!< this is the total number of lines not part of the image and is used for vertical screen size
|
int m_numberOfBlackLines; //!< this is the total number of lines not part of the image and is used for vertical screen size
|
||||||
int m_numberOfEqLines; //!< number of equalizing lines both whole and partial
|
int m_firstVisibleLine;
|
||||||
|
|
||||||
|
int m_fieldDetectStartPos;
|
||||||
|
int m_fieldDetectEndPos;
|
||||||
|
int m_vSyncDetectStartPos;
|
||||||
|
int m_vSyncDetectEndPos;
|
||||||
|
|
||||||
|
int m_vSyncDetectThreshold;
|
||||||
|
int m_fieldDetectThreshold1;
|
||||||
|
int m_fieldDetectThreshold2;
|
||||||
|
|
||||||
|
int m_numberOfVSyncLines;
|
||||||
int m_numberSamplesPerLineSignals; //!< number of samples in the non image part of the line (signals = front porch + pulse + back porch)
|
int m_numberSamplesPerLineSignals; //!< number of samples in the non image part of the line (signals = front porch + pulse + back porch)
|
||||||
int m_numberSamplesPerHSync; //!< number of samples per horizontal synchronization pattern (pulse + back porch)
|
int m_numberSamplesPerHSync; //!< number of samples per horizontal synchronization pattern (pulse + back porch)
|
||||||
int m_numberSamplesHSyncCrop; //!< number of samples to crop from start of horizontal synchronization
|
int m_numberSamplesHSyncCrop; //!< number of samples to crop from start of horizontal synchronization
|
||||||
bool m_interleaved; //!< interleaved image
|
bool m_interleaved; //!< interleaved image
|
||||||
int m_firstRowIndexEven; //!< index of the first row of an even image
|
|
||||||
int m_firstRowIndexOdd; //!< index of the first row of an even image
|
|
||||||
|
|
||||||
//*************** PROCESSING ***************
|
//*************** PROCESSING ***************
|
||||||
|
|
||||||
int m_imageIndex;
|
int m_fieldIndex;
|
||||||
int m_synchroSamples;
|
int m_synchroSamples;
|
||||||
|
|
||||||
bool m_verticalSynchroDetected;
|
int m_fieldDetectSampleCount;
|
||||||
|
int m_vSyncDetectSampleCount;
|
||||||
float m_ampLineSum;
|
|
||||||
float m_ampLineAvg;
|
|
||||||
|
|
||||||
float m_effMin;
|
float m_effMin;
|
||||||
float m_effMax;
|
float m_effMax;
|
||||||
|
@ -144,12 +149,18 @@ private:
|
||||||
float m_fltBufferQ[6];
|
float m_fltBufferQ[6];
|
||||||
|
|
||||||
int m_colIndex;
|
int m_colIndex;
|
||||||
int m_sampleIndex;
|
int m_sampleIndex; // assumed (averaged) sample offset from the start of horizontal sync pulse
|
||||||
|
int m_sampleIndexDetected; // detected sample offset from the start of horizontal sync pulse
|
||||||
int m_amSampleIndex;
|
int m_amSampleIndex;
|
||||||
int m_rowIndex;
|
int m_rowIndex;
|
||||||
int m_lineIndex;
|
int m_lineIndex;
|
||||||
|
|
||||||
AvgExpInt m_objAvgColIndex;
|
float m_hSyncShiftSum;
|
||||||
|
int m_hSyncShiftCount;
|
||||||
|
int m_hSyncErrorCount;
|
||||||
|
|
||||||
|
float prevSample;
|
||||||
|
|
||||||
int m_avgColIndex;
|
int m_avgColIndex;
|
||||||
|
|
||||||
SampleVector m_sampleBuffer;
|
SampleVector m_sampleBuffer;
|
||||||
|
@ -212,7 +223,6 @@ private:
|
||||||
m_avgColIndex = m_colIndex;
|
m_avgColIndex = m_colIndex;
|
||||||
m_registeredTVScreen->renderImage(0);
|
m_registeredTVScreen->renderImage(0);
|
||||||
|
|
||||||
m_imageIndex++;
|
|
||||||
m_lineIndex = 0;
|
m_lineIndex = 0;
|
||||||
m_rowIndex = 0;
|
m_rowIndex = 0;
|
||||||
m_registeredTVScreen->selectRow(m_rowIndex);
|
m_registeredTVScreen->selectRow(m_rowIndex);
|
||||||
|
@ -247,136 +257,120 @@ private:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vertical sync is obtained when the average level of signal on a line is below a certain threshold. This is obtained by lowering signal to ultra black during at least 3/4th of the line
|
|
||||||
// We use directly the sum of line sample values
|
|
||||||
inline void processClassic(float& sample, int& sampleVideo)
|
inline void processClassic(float& sample, int& sampleVideo)
|
||||||
{
|
{
|
||||||
// Filling pixel on the current line - reference index 0 at start of sync pulse
|
// Filling pixel on the current line - reference index 0 at start of sync pulse
|
||||||
// remove only sync pulse empirically, +4 is to compensate shift due to hsync amortizing factor of 1/4
|
m_registeredTVScreen->setDataColor(m_sampleIndex - m_numberSamplesPerHSync, sampleVideo, sampleVideo, sampleVideo);
|
||||||
m_registeredTVScreen->setDataColor(m_colIndex - m_numberSamplesPerHSync + m_numberSamplesPerHTop, sampleVideo, sampleVideo, sampleVideo);
|
|
||||||
|
|
||||||
int synchroTimeSamples = (3 * m_samplesPerLine) / 4; // count 3/4 line globally
|
|
||||||
float synchroTrameLevel = 0.5f * ((float) synchroTimeSamples) * m_settings.m_levelBlack; // threshold is half the black value over 3/4th of line samples
|
|
||||||
|
|
||||||
|
if (m_settings.m_hSync)
|
||||||
|
{
|
||||||
// Horizontal Synchro detection
|
// Horizontal Synchro detection
|
||||||
|
if ((prevSample >= m_settings.m_levelSynchroTop &&
|
||||||
// Floor Detection 0
|
sample < m_settings.m_levelSynchroTop) // horizontal synchro detected
|
||||||
if (sample < m_settings.m_levelSynchroTop)
|
&& (m_sampleIndexDetected > m_samplesPerLine - m_numberSamplesPerHTopNom))
|
||||||
{
|
{
|
||||||
m_synchroSamples++;
|
double sampleIndexDetectedFrac =
|
||||||
}
|
(sample - m_settings.m_levelSynchroTop) / (prevSample - sample);
|
||||||
// Black detection 0.3
|
double hSyncShift = -m_sampleIndex - sampleIndexDetectedFrac;
|
||||||
else if (sample > m_settings.m_levelBlack) {
|
if (hSyncShift > m_samplesPerLine / 2)
|
||||||
m_synchroSamples = 0;
|
hSyncShift -= m_samplesPerLine;
|
||||||
}
|
else if (hSyncShift < -m_samplesPerLine / 2)
|
||||||
|
hSyncShift += m_samplesPerLine;
|
||||||
|
|
||||||
//Horizontal Synchro processing
|
if (fabs(hSyncShift) > m_numberSamplesPerHTopNom)
|
||||||
if ((m_synchroSamples == m_numberSamplesPerHTop) // horizontal synchro detected
|
|
||||||
&& (m_sampleIndex > (m_samplesPerLine/2) + m_numberSamplesPerLineSignals))
|
|
||||||
{
|
{
|
||||||
m_avgColIndex = m_sampleIndex - m_colIndex;
|
m_hSyncErrorCount++;
|
||||||
//qDebug("HSync: %d %d %d", m_sampleIndex, m_colIndex, m_avgColIndex);
|
if (m_hSyncErrorCount >= 8)
|
||||||
|
{
|
||||||
|
// Fast sync: shift is too large, needs to be fixed ASAP
|
||||||
m_sampleIndex = 0;
|
m_sampleIndex = 0;
|
||||||
|
m_hSyncShiftSum = 0.0;
|
||||||
|
m_hSyncShiftCount = 0;
|
||||||
|
m_hSyncErrorCount = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
m_hSyncShiftSum += hSyncShift;
|
||||||
|
m_hSyncShiftCount++;
|
||||||
|
m_hSyncErrorCount = 0;
|
||||||
|
}
|
||||||
|
m_sampleIndexDetected = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
m_sampleIndexDetected++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_hSyncShiftSum = 0.0f;
|
||||||
|
m_hSyncShiftCount = 0;
|
||||||
|
}
|
||||||
m_sampleIndex++;
|
m_sampleIndex++;
|
||||||
}
|
|
||||||
|
|
||||||
if (m_colIndex < m_samplesPerLine + m_numberSamplesPerHTop - 1) // increment until full line + next horizontal pulse
|
if (m_settings.m_vSync)
|
||||||
{
|
{
|
||||||
m_colIndex++;
|
if (m_sampleIndex > m_fieldDetectStartPos && m_sampleIndex < m_fieldDetectEndPos)
|
||||||
|
m_fieldDetectSampleCount += sample < m_settings.m_levelSynchroTop;
|
||||||
|
if (m_sampleIndex > m_vSyncDetectStartPos && m_sampleIndex < m_vSyncDetectEndPos)
|
||||||
|
m_vSyncDetectSampleCount += sample < m_settings.m_levelSynchroTop;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_colIndex < (m_samplesPerLine/2)) { // count on first half of line for better separation between black and ultra black
|
// end of line
|
||||||
m_ampLineSum += sample;
|
if (m_sampleIndex >= m_samplesPerLine)
|
||||||
}
|
|
||||||
}
|
|
||||||
else // full line + next horizontal pulse => start of screen reference line
|
|
||||||
{
|
{
|
||||||
m_ampLineAvg = m_ampLineSum / ((m_samplesPerLine/2) - m_numberSamplesPerHTop); // avg length is half line less horizontal top
|
m_sampleIndex = 0;
|
||||||
m_ampLineSum = 0.0f;
|
m_lineIndex++;
|
||||||
|
|
||||||
// set column index to start a new line
|
if (m_lineIndex == m_numberOfVSyncLines + 3 && m_fieldIndex == 0)
|
||||||
if (m_settings.m_hSync && (m_lineIndex == 0)) {
|
|
||||||
m_colIndex = m_numberSamplesPerHTop + m_avgColIndex/4; // amortizing 1/4
|
|
||||||
} else {
|
|
||||||
m_colIndex = m_numberSamplesPerHTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
// process line
|
|
||||||
m_lineIndex++; // new line
|
|
||||||
m_rowIndex += m_interleaved ? 2 : 1; // new row considering interleaving
|
|
||||||
|
|
||||||
if (m_rowIndex < m_settings.m_nbLines) {
|
|
||||||
m_registeredTVScreen->selectRow(m_rowIndex - m_numberOfSyncLines);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vertical sync and image rendering
|
|
||||||
|
|
||||||
if (m_lineIndex > m_numberOfBlackLines) {
|
|
||||||
m_verticalSynchroDetected = false; // reset trigger when detection zone is left
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((m_settings.m_vSync) && (m_lineIndex <= m_settings.m_nbLines)) // VSync activated and lines in range
|
|
||||||
{
|
{
|
||||||
if (m_colIndex >= synchroTimeSamples)
|
float shiftSamples = 0.0f;
|
||||||
|
|
||||||
|
// Slow sync: slight adjustment is needed
|
||||||
|
if (m_hSyncShiftCount != 0 && m_settings.m_hSync)
|
||||||
{
|
{
|
||||||
if (m_ampLineAvg < 0.15f) // ultra black detection
|
shiftSamples = m_hSyncShiftSum / m_hSyncShiftCount;
|
||||||
|
m_sampleIndex = shiftSamples;
|
||||||
|
m_hSyncShiftSum = 0.0f;
|
||||||
|
m_hSyncShiftCount = 0;
|
||||||
|
m_hSyncErrorCount = 0;
|
||||||
|
}
|
||||||
|
m_registeredTVScreen->renderImage(0,
|
||||||
|
shiftSamples < -1.0f ? -1.0f : (shiftSamples > 1.0f ? 1.0f : shiftSamples));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_vSyncDetectSampleCount > m_vSyncDetectThreshold &&
|
||||||
|
(m_lineIndex < 3 || m_lineIndex > m_numberOfVSyncLines + 1) && m_settings.m_vSync)
|
||||||
{
|
{
|
||||||
if (!m_verticalSynchroDetected) // not yet
|
if (m_interleaved)
|
||||||
{
|
{
|
||||||
m_verticalSynchroDetected = true; // prevent repetition
|
if (m_fieldDetectSampleCount > m_fieldDetectThreshold1)
|
||||||
|
m_fieldIndex = 0;
|
||||||
|
else if (m_fieldDetectSampleCount < m_fieldDetectThreshold2)
|
||||||
|
m_fieldIndex = 1;
|
||||||
|
}
|
||||||
|
m_lineIndex = 2;
|
||||||
|
}
|
||||||
|
m_fieldDetectSampleCount = 0;
|
||||||
|
m_vSyncDetectSampleCount = 0;
|
||||||
|
|
||||||
// Odd frame or not interleaved
|
if (m_lineIndex > m_settings.m_nbLines / 2 + m_fieldIndex && m_interleaved)
|
||||||
if ((m_imageIndex % 2 == 1) || !m_interleaved) {
|
|
||||||
m_registeredTVScreen->renderImage(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_lineIndex > m_settings.m_nbLines/2) { // long frame done (even)
|
|
||||||
m_imageIndex = m_firstRowIndexOdd; // next is odd
|
|
||||||
} else {
|
|
||||||
m_imageIndex = m_firstRowIndexEven; // next is even
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_interleaved) {
|
|
||||||
m_rowIndex = m_imageIndex;
|
|
||||||
} else {
|
|
||||||
m_rowIndex = 0; // just the first line
|
|
||||||
}
|
|
||||||
|
|
||||||
// qDebug("ATVDemodSink::processClassic: m_lineIndex: %d m_imageIndex: %d m_rowIndex: %d",
|
|
||||||
// m_lineIndex, m_imageIndex, m_rowIndex);
|
|
||||||
m_registeredTVScreen->selectRow(m_rowIndex - m_numberOfSyncLines);
|
|
||||||
|
|
||||||
m_lineIndex = 0;
|
|
||||||
m_imageIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else // no VSync or lines out of range => set new image arbitrarily
|
|
||||||
{
|
{
|
||||||
if (m_lineIndex >= m_settings.m_nbLines/2)
|
m_lineIndex = 1;
|
||||||
|
m_fieldIndex = 1 - m_fieldIndex;
|
||||||
|
}
|
||||||
|
else if (m_lineIndex > m_settings.m_nbLines && !m_interleaved)
|
||||||
{
|
{
|
||||||
if (m_lineIndex > m_settings.m_nbLines/2) { // long frame done (even)
|
m_lineIndex = 1;
|
||||||
m_imageIndex = m_firstRowIndexOdd; // next is odd
|
m_fieldIndex = 0;
|
||||||
} else {
|
|
||||||
m_imageIndex = m_firstRowIndexEven; // next is even
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_interleaved) {
|
int rowIndex = m_lineIndex - m_firstVisibleLine;
|
||||||
m_rowIndex = m_imageIndex;
|
if (m_interleaved)
|
||||||
} else {
|
rowIndex = rowIndex * 2 - m_fieldIndex;
|
||||||
m_rowIndex = 0; // just the first line
|
m_registeredTVScreen->selectRow(rowIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_registeredTVScreen->selectRow(m_rowIndex - m_numberOfSyncLines);
|
prevSample = sample;
|
||||||
|
|
||||||
m_lineIndex = 0;
|
|
||||||
m_imageIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -39,11 +39,13 @@ GLShaderTVArray::GLShaderTVArray(bool blnColor) : m_blnColor(blnColor)
|
||||||
{
|
{
|
||||||
m_blnAlphaBlend = false;
|
m_blnAlphaBlend = false;
|
||||||
m_blnAlphaReset = false;
|
m_blnAlphaReset = false;
|
||||||
|
m_blnExtraColumns = false;
|
||||||
m_objProgram = 0;
|
m_objProgram = 0;
|
||||||
m_objImage = 0;
|
m_objImage = 0;
|
||||||
m_objTexture = 0;
|
m_objTexture = 0;
|
||||||
m_intCols = 0;
|
m_intCols = 0;
|
||||||
m_intRows = 0;
|
m_intRows = 0;
|
||||||
|
m_subsampleShift = 0.0f;
|
||||||
m_blnInitialized = false;
|
m_blnInitialized = false;
|
||||||
m_objCurrentRow = 0;
|
m_objCurrentRow = 0;
|
||||||
|
|
||||||
|
@ -110,7 +112,8 @@ void GLShaderTVArray::InitializeGL(int intCols, int intRows)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Image container
|
//Image container
|
||||||
m_objImage = new QImage(intCols, intRows, QImage::Format_RGBA8888);
|
int cols = intCols + (m_blnExtraColumns ? 2 : 0);
|
||||||
|
m_objImage = new QImage(cols, intRows, QImage::Format_RGBA8888);
|
||||||
m_objImage->fill(QColor(0, 0, 0));
|
m_objImage->fill(QColor(0, 0, 0));
|
||||||
|
|
||||||
m_objTexture = new QOpenGLTexture(*m_objImage);
|
m_objTexture = new QOpenGLTexture(*m_objImage);
|
||||||
|
@ -155,11 +158,23 @@ void GLShaderTVArray::RenderPixels(unsigned char *chrData)
|
||||||
|
|
||||||
QMatrix4x4 objQMatrix;
|
QMatrix4x4 objQMatrix;
|
||||||
|
|
||||||
|
float rectHalfWidth = 1.0f;
|
||||||
|
float sampleSize = 2.0f / m_intCols;
|
||||||
|
if (m_blnExtraColumns)
|
||||||
|
rectHalfWidth += sampleSize;
|
||||||
|
float xShift = sampleSize * m_subsampleShift;
|
||||||
|
|
||||||
GLfloat arrVertices[] =
|
GLfloat arrVertices[] =
|
||||||
// 2 3
|
// 2 3
|
||||||
// 1 4
|
// 1 4
|
||||||
//1 2 3 3 4 1
|
{
|
||||||
{ -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, -1.0f };
|
-rectHalfWidth + xShift, -1.0f, // 1
|
||||||
|
-rectHalfWidth + xShift, 1.0f, // 2
|
||||||
|
rectHalfWidth + xShift, 1.0f, // 3
|
||||||
|
rectHalfWidth + xShift, 1.0f, // 3
|
||||||
|
rectHalfWidth + xShift, -1.0f, // 4
|
||||||
|
-rectHalfWidth + xShift, -1.0f // 1
|
||||||
|
};
|
||||||
|
|
||||||
GLfloat arrTextureCoords[] =
|
GLfloat arrTextureCoords[] =
|
||||||
// 1 4
|
// 1 4
|
||||||
|
@ -227,7 +242,8 @@ void GLShaderTVArray::RenderPixels(unsigned char *chrData)
|
||||||
|
|
||||||
m_objTexture->bind();
|
m_objTexture->bind();
|
||||||
|
|
||||||
ptrF->glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_intCols, m_intRows, GL_RGBA,
|
ptrF->glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0,
|
||||||
|
m_intCols + (m_blnExtraColumns ? 2 : 0), m_intRows, GL_RGBA,
|
||||||
GL_UNSIGNED_BYTE, m_objImage->bits());
|
GL_UNSIGNED_BYTE, m_objImage->bits());
|
||||||
|
|
||||||
ptrF->glEnableVertexAttribArray(0); // vertex
|
ptrF->glEnableVertexAttribArray(0); // vertex
|
||||||
|
@ -318,9 +334,11 @@ bool GLShaderTVArray::SetDataColor(int intCol, QRgb objColor)
|
||||||
|
|
||||||
if (m_blnInitialized)
|
if (m_blnInitialized)
|
||||||
{
|
{
|
||||||
if ((intCol < m_intCols) && (intCol >= 0) && (m_objCurrentRow != 0))
|
if ((intCol < m_intCols + m_blnExtraColumns) &&
|
||||||
|
(intCol >= -m_blnExtraColumns) &&
|
||||||
|
(m_objCurrentRow != 0))
|
||||||
{
|
{
|
||||||
m_objCurrentRow[intCol] = objColor;
|
m_objCurrentRow[intCol + m_blnExtraColumns] = objColor;
|
||||||
blnRslt = true;
|
blnRslt = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,8 @@ public:
|
||||||
GLShaderTVArray(bool blnColor);
|
GLShaderTVArray(bool blnColor);
|
||||||
~GLShaderTVArray();
|
~GLShaderTVArray();
|
||||||
|
|
||||||
|
void setExtraColumns(bool blnExtraColumns) { m_blnExtraColumns = blnExtraColumns; }
|
||||||
|
void setSubsampleShift(float subsampleShift) { m_subsampleShift = subsampleShift; }
|
||||||
void setColor(bool blnColor) { m_blnColor = blnColor; }
|
void setColor(bool blnColor) { m_blnColor = blnColor; }
|
||||||
void setAlphaBlend(bool blnAlphaBlend) { m_blnAlphaBlend = blnAlphaBlend; }
|
void setAlphaBlend(bool blnAlphaBlend) { m_blnAlphaBlend = blnAlphaBlend; }
|
||||||
void setAlphaReset() { m_blnAlphaReset = true; }
|
void setAlphaReset() { m_blnAlphaReset = true; }
|
||||||
|
@ -71,6 +73,7 @@ protected:
|
||||||
|
|
||||||
int m_intCols;
|
int m_intCols;
|
||||||
int m_intRows;
|
int m_intRows;
|
||||||
|
float m_subsampleShift;
|
||||||
|
|
||||||
QRgb * m_objCurrentRow;
|
QRgb * m_objCurrentRow;
|
||||||
|
|
||||||
|
@ -78,6 +81,7 @@ protected:
|
||||||
bool m_blnColor;
|
bool m_blnColor;
|
||||||
bool m_blnAlphaBlend;
|
bool m_blnAlphaBlend;
|
||||||
bool m_blnAlphaReset;
|
bool m_blnAlphaReset;
|
||||||
|
bool m_blnExtraColumns;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif /* INCLUDE_GUI_GLTVSHADERARRAY_H_ */
|
#endif /* INCLUDE_GUI_GLTVSHADERARRAY_H_ */
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
|
||||||
|
// Note: When this object is created, QWidget* is converted to bool
|
||||||
TVScreen::TVScreen(bool blnColor, QWidget* parent) :
|
TVScreen::TVScreen(bool blnColor, QWidget* parent) :
|
||||||
QGLWidget(parent), m_objMutex(QMutex::NonRecursive), m_objGLShaderArray(blnColor)
|
QGLWidget(parent), m_objMutex(QMutex::NonRecursive), m_objGLShaderArray(blnColor)
|
||||||
{
|
{
|
||||||
|
@ -36,6 +37,7 @@ TVScreen::TVScreen(bool blnColor, QWidget* parent) :
|
||||||
m_objTimer.start(40); // capped at 25 FPS
|
m_objTimer.start(40); // capped at 25 FPS
|
||||||
|
|
||||||
m_chrLastData = NULL;
|
m_chrLastData = NULL;
|
||||||
|
m_subsampleShift = 0.0;
|
||||||
m_blnConfigChanged = false;
|
m_blnConfigChanged = false;
|
||||||
m_blnDataChanged = false;
|
m_blnDataChanged = false;
|
||||||
m_blnGLContextInitialized = false;
|
m_blnGLContextInitialized = false;
|
||||||
|
@ -57,6 +59,11 @@ void TVScreen::setColor(bool blnColor)
|
||||||
m_objGLShaderArray.setColor(blnColor);
|
m_objGLShaderArray.setColor(blnColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TVScreen::setExtraColumns(bool blnExtraColumns)
|
||||||
|
{
|
||||||
|
m_objGLShaderArray.setExtraColumns(blnExtraColumns);
|
||||||
|
}
|
||||||
|
|
||||||
QRgb* TVScreen::getRowBuffer(int intRow)
|
QRgb* TVScreen::getRowBuffer(int intRow)
|
||||||
{
|
{
|
||||||
if (!m_blnGLContextInitialized)
|
if (!m_blnGLContextInitialized)
|
||||||
|
@ -67,9 +74,10 @@ QRgb* TVScreen::getRowBuffer(int intRow)
|
||||||
return m_objGLShaderArray.GetRowBuffer(intRow);
|
return m_objGLShaderArray.GetRowBuffer(intRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TVScreen::renderImage(unsigned char * objData)
|
void TVScreen::renderImage(unsigned char * objData, float subsampleShift)
|
||||||
{
|
{
|
||||||
m_chrLastData = objData;
|
m_chrLastData = objData;
|
||||||
|
m_subsampleShift = subsampleShift;
|
||||||
m_blnDataChanged = true;
|
m_blnDataChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,6 +184,7 @@ void TVScreen::paintGL()
|
||||||
m_intAskedRows = 0;
|
m_intAskedRows = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_objGLShaderArray.setSubsampleShift(m_subsampleShift);
|
||||||
m_objGLShaderArray.RenderPixels(m_chrLastData);
|
m_objGLShaderArray.RenderPixels(m_chrLastData);
|
||||||
|
|
||||||
m_objMutex.unlock();
|
m_objMutex.unlock();
|
||||||
|
|
|
@ -46,9 +46,10 @@ public:
|
||||||
virtual ~TVScreen();
|
virtual ~TVScreen();
|
||||||
|
|
||||||
void setColor(bool blnColor);
|
void setColor(bool blnColor);
|
||||||
|
void setExtraColumns(bool blnExtraColumns);
|
||||||
void resizeTVScreen(int intCols, int intRows);
|
void resizeTVScreen(int intCols, int intRows);
|
||||||
void getSize(int& intCols, int& intRows) const;
|
void getSize(int& intCols, int& intRows) const;
|
||||||
void renderImage(unsigned char * objData);
|
void renderImage(unsigned char * objData, float subsampleShift = 0.0);
|
||||||
QRgb* getRowBuffer(int intRow);
|
QRgb* getRowBuffer(int intRow);
|
||||||
void resetImage();
|
void resetImage();
|
||||||
void resetImage(int alpha);
|
void resetImage(int alpha);
|
||||||
|
@ -73,6 +74,7 @@ private:
|
||||||
bool m_blnGLContextInitialized;
|
bool m_blnGLContextInitialized;
|
||||||
int m_intAskedCols;
|
int m_intAskedCols;
|
||||||
int m_intAskedRows;
|
int m_intAskedRows;
|
||||||
|
float m_subsampleShift;
|
||||||
|
|
||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
Loading…
Reference in New Issue