-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathFrameBuilder.inl
594 lines (548 loc) · 29.8 KB
/
FrameBuilder.inl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
#include "SplineHelpers.h"
#include "StatisticsHelpers.h"
#include <sstream>
//----------------------------------------------------------------------------------------------------------------------
// Frame detection methods
//----------------------------------------------------------------------------------------------------------------------
template<class SampleType>
std::list<FrameBuilder::FieldInfo> FrameBuilder::DetectFields(const std::vector<SampleType>& sampleData, const std::list<SyncDetector::SyncInfo>& syncEvents) const
{
// Build a list of complete fields from the list of sync events
std::list<FieldInfo> fields;
FieldInfo currentField;
bool currentFieldHasKnownStartPos = false;
bool foundHSyncInCurrentField = false;
unsigned int lineCount = 0;
size_t lastSyncEventCount = 0;
for (auto syncInfo : syncEvents)
{
// If this is a vertical sync event and we've found at least one horizontal sync event in the current field,
// we've found the start of a new field. In this case, we need to complete the last field and reset the current
// field.
//##FIX## NTSC signal specs state line 1 starts with the first equalizing pulse, while PAL and SECAM specs state
//that line 1 starts with the first vsync pulse. We currently implement the NTSC approach here, but this should
//be selectable by the user.
if ((syncInfo.type != SyncDetector::SyncType::Horizontal) && foundHSyncInCurrentField)
{
// If the current field has a known start position, add it to the list of complete fields.
if (currentFieldHasKnownStartPos)
{
lastSyncEventCount = currentField.syncEvents.size();
currentField.lineCount = lineCount;
currentField.endSampleNo = syncInfo.startSampleNo;
currentField.followingSyncEvent = syncInfo;
fields.push_back(std::move(currentField));
}
// Reset the current field
currentField = FieldInfo();
currentField.syncEvents.reserve(lastSyncEventCount);
currentFieldHasKnownStartPos = true;
foundHSyncInCurrentField = false;
lineCount = 0;
}
// If this is a horizontal sync event, increment the line count for this field.
if (syncInfo.type == SyncDetector::SyncType::Horizontal)
{
++lineCount;
}
// Flag whether we've found a hsync event in the current field
foundHSyncInCurrentField |= (syncInfo.type == SyncDetector::SyncType::Horizontal);
// If the current field doesn't have a known start position, advance to the next sync event.
if (!currentFieldHasKnownStartPos)
{
continue;
}
// Add this sync event to the list of sync events for the current field
currentField.syncEvents.push_back(syncInfo);
}
// Return the list of fields to the caller
return fields;
}
//----------------------------------------------------------------------------------------------------------------------
template<class SampleType>
void FrameBuilder::DetectLines(const std::vector<SampleType>& sampleData, std::vector<FrameInfo>& frames, unsigned int threadCount) const
{
// Determine the number of threads to use for this operation
size_t frameCount = frames.size();
if (threadCount <= 0)
{
unsigned int coreCount = std::thread::hardware_concurrency();
threadCount = (coreCount > 0) ? coreCount : 4;
}
if (threadCount > frameCount)
{
threadCount = 1;
}
// Spawn worker threads to perform line detection in parallel
size_t chunkCount = threadCount;
size_t framesPerChunk = frameCount / chunkCount;
size_t extraFrames = frameCount - (framesPerChunk * chunkCount);
std::vector<std::thread> workerThreads;
for (unsigned int i = 0; i < chunkCount; ++i)
{
workerThreads.emplace_back(std::thread([&, i]
{
size_t frameNo = (i * framesPerChunk) + (i > 0 ? extraFrames : 0);
size_t lastFrameNoForChunk = frameNo + (framesPerChunk + (i == 0 ? extraFrames : 0));
while (frameNo < lastFrameNoForChunk)
{
for (FrameBuilder::FieldInfo& fieldInfo : frames[frameNo].fields)
{
DetectLines(sampleData, fieldInfo);
}
++frameNo;
}
}));
}
for (auto& entry : workerThreads)
{
entry.join();
}
}
//----------------------------------------------------------------------------------------------------------------------
template<class SampleType>
void FrameBuilder::DetectLines(const std::vector<SampleType>& sampleData, FieldInfo& fieldInfo) const
{
// Reserve enough space in the line buffer for up to the number of sync events we have in the field
fieldInfo.lines.reserve(fieldInfo.syncEvents.size());
// Build a set of lines from the sync events in this field
auto syncEventIterator = fieldInfo.syncEvents.cbegin();
auto syncEventStartLineIterator = syncEventIterator;
while (syncEventIterator != fieldInfo.syncEvents.cend())
{
bool foundHalfLineEvent = false;
while ((syncEventIterator != fieldInfo.syncEvents.cend()) && (syncEventIterator->type != SyncDetector::SyncType::Horizontal))
{
bool syncEventIsHalfLineEvent = (syncEventIterator->type == SyncDetector::SyncType::Equalizer) || (syncEventIterator->type == SyncDetector::SyncType::Vertical);
if (foundHalfLineEvent && syncEventIsHalfLineEvent)
{
break;
}
foundHalfLineEvent |= syncEventIsHalfLineEvent;
++syncEventIterator;
}
if (syncEventIterator != fieldInfo.syncEvents.cend())
{
++syncEventIterator;
const SyncDetector::SyncInfo& nextSyncEvent = (syncEventIterator != fieldInfo.syncEvents.cend()) ? *syncEventIterator : fieldInfo.followingSyncEvent;
LineInfo lineInfo = {};
lineInfo.leadingSyncInfo = *syncEventStartLineIterator;
lineInfo.followingSyncInfo = nextSyncEvent;
fieldInfo.lines.emplace_back(std::move(lineInfo));
syncEventStartLineIterator = syncEventIterator;
}
}
// Process each detected line, building advanced information for each of them.
for (LineInfo& lineInfo : fieldInfo.lines)
{
lineInfo.backPorchStartPos = FindSyncRisingEdgeSamplePos(sampleData, lineInfo.leadingSyncInfo);
lineInfo.frontPorchEndPos = FindSyncFallingEdgeSamplePos(sampleData, lineInfo.followingSyncInfo);
double backPorchFlatStartPos = FindSyncRisingEdgeEndSamplePos(sampleData, lineInfo.leadingSyncInfo, lineInfo.backPorchStartPos);
// If this line contains active video, calculate an upsampled back porch region to perform more detailed
// analysis on.
std::vector<SampleType> upsampledBackPorch;
double upsampledBackPorchStartPos;
double upsampledBackPorchEndPos;
if (lineInfo.leadingSyncInfo.type == SyncDetector::SyncType::Horizontal)
{
size_t hsyncLength = lineInfo.leadingSyncInfo.endSampleNo - lineInfo.leadingSyncInfo.startSampleNo;
double backPorchSafeLength = (double)hsyncLength * syncLengthToBackPorchMinRatio;
upsampledBackPorchStartPos = backPorchFlatStartPos;
upsampledBackPorchEndPos = upsampledBackPorchStartPos + backPorchSafeLength;
upsampledBackPorch.resize((size_t)(backPorchSafeLength * blankingUpsampleRatio));
CubicInterpolateCatmullRom(sampleData.data(), upsampledBackPorchStartPos, upsampledBackPorchEndPos, upsampledBackPorch);
}
// Calculate the average level of the back porch region for the line
if (lineInfo.leadingSyncInfo.type == SyncDetector::SyncType::Horizontal)
{
lineInfo.averageBlankingLevel = FindMedianValue(upsampledBackPorch.begin(), upsampledBackPorch.end());
}
else
{
//##TODO## Consider doing something better here. We could either try and calculate it, or take the blanking
//level of the first following line in active scan.
size_t backPorchStartSampleNo = (size_t)(backPorchFlatStartPos + 0.5);
lineInfo.averageBlankingLevel = sampleData[backPorchStartSampleNo];
}
// Calculate the average level of the leading sync region for the line
lineInfo.averageSyncLevel = FindMedianValue(sampleData.begin() + lineInfo.leadingSyncInfo.startSampleNo, sampleData.begin() + lineInfo.leadingSyncInfo.endSampleNo);
// Calculate the IRE levels for this line from the average sync and blanking levels. Sync occurs at IRE -40, and
// blanking occurs at IRE 0. From these two reference levels, we can establish our IRE scale.
//##FIX## In a PAL signal, sync occurs at -43 IRE, and the colour burst has a 21.5 IRE amplitude with 10 cycles.
lineInfo.ireLevel0 = lineInfo.averageBlankingLevel;
lineInfo.ireLevel100 = lineInfo.averageBlankingLevel + ((lineInfo.averageBlankingLevel - lineInfo.averageSyncLevel) * (1.0 / 0.4));
// Attempt to locate a colour burst signal in the back porch region of the line
if (lineInfo.leadingSyncInfo.type == SyncDetector::SyncType::Horizontal)
{
// Attempt to detect the color burst for this line
//##TODO## Handle this properly if the color burst is completely absent. We shouldn't consider monochrome
//signals to be "broken".
SampleType colorBurstMinAmplitude = (SampleType)((lineInfo.ireLevel100 - lineInfo.ireLevel0) * colorBurstIREDetectionThreshold);
DetectColorBurst(upsampledBackPorch, (SampleType)lineInfo.ireLevel0, colorBurstMinAmplitude, lineInfo.colorBurstWaves);
lineInfo.colorBurstValid = ValidateColorBurst(colorBurstMinimumHalfOscillationCount, lineInfo.colorBurstWaves);
// If the colour burst isn't valid, attempt to repair it.
if (!lineInfo.colorBurstValid)
{
lineInfo.colorBurstValid = RepairColorBurst(upsampledBackPorch, (SampleType)lineInfo.ireLevel0, colorBurstMinAmplitude, lineInfo.colorBurstWaves);
}
// If the colour burst repair failed, report an error.
//##TODO## Add an extra processing step after line detection to predict missing colour bursts from
//surrounding lines in the field. Do the same for fields to predict broken vsync signals, and for lines to
//predict broken hsync pulses.
if (!lineInfo.colorBurstValid)
{
_log.Error("Failed to detect colour burst on hsync!\t{0}\t{1}\t{2}\t{3}", lineInfo.leadingSyncInfo.startSampleNo, backPorchFlatStartPos, lineInfo.backPorchStartPos, lineInfo.frontPorchEndPos);
}
// Convert our colour burst wave positions back to absolute sample positions
for (ColorBurstWaveInfo& waveInfo : lineInfo.colorBurstWaves)
{
waveInfo.startPos = upsampledBackPorchStartPos + (waveInfo.startPos / blankingUpsampleRatio);
waveInfo.endPos = upsampledBackPorchStartPos + (waveInfo.endPos / blankingUpsampleRatio);
}
}
}
// Perform more precise sync correction using the phase of the colour burst if requested. Note that we do this here
// rather than leaving it to caller to perform this extra step, as we handle automatic threading on this method, and
// it's more efficient to perform this operation within the worker threads while line detection is being done in
// parallel.
if (useColorBurstForLineSyncCorrection)
{
PerformColorBurstLineSyncCorrection(fieldInfo);
}
}
//----------------------------------------------------------------------------------------
// Trigger detection methods
//----------------------------------------------------------------------------------------------------------------------
template<class SampleType>
double FrameBuilder::FindSyncRisingEdgeSamplePos(const std::vector<SampleType>& inputData, const SyncDetector::SyncInfo& syncInfo) const
{
// Find the point at which we cross the minimum threshold for ending the sync run
double syncEndMinimumThreshold = (SampleType)(syncInfo.averageSyncLevel + (syncInfo.approxMinMaxSampleRange * syncAmplitudeMinTolerance));
SampleType syncEndMinimumThresholdAsSampleType = (SampleType)syncEndMinimumThreshold;
size_t syncEndSearchPos = syncInfo.endSampleNo;
while (inputData[syncEndSearchPos] < syncEndMinimumThresholdAsSampleType)
{
++syncEndSearchPos;
}
double sampleOffset = CreateSplineCatmullRomUniform((double)inputData[syncEndSearchPos - 2], (double)inputData[syncEndSearchPos - 1], (double)inputData[syncEndSearchPos], (double)inputData[syncEndSearchPos + 1]).Reverse(syncEndMinimumThreshold, slopeDetectionTolerance);
return (double)(syncEndSearchPos - 1) + sampleOffset;
}
//----------------------------------------------------------------------------------------------------------------------
template<class SampleType>
double FrameBuilder::FindSyncFallingEdgeSamplePos(const std::vector<SampleType>& inputData, const SyncDetector::SyncInfo& syncInfo) const
{
// Find the point at which we cross the minimum threshold for starting the sync run
double syncStartMinimumThreshold = (syncInfo.averageSyncLevel + (syncInfo.approxMinMaxSampleRange * syncAmplitudeMinTolerance));
SampleType syncStartMinimumThresholdAsSampleType = (SampleType)syncStartMinimumThreshold;
size_t syncStartSearchPos = syncInfo.startSampleNo;
while (inputData[syncStartSearchPos] < syncStartMinimumThreshold)
{
--syncStartSearchPos;
}
double sampleOffset = CreateSplineCatmullRomUniform((double)inputData[syncStartSearchPos - 1], (double)inputData[syncStartSearchPos], (double)inputData[syncStartSearchPos + 1], (double)inputData[syncStartSearchPos + 2]).Reverse(syncStartMinimumThreshold, slopeDetectionTolerance);
return (double)syncStartSearchPos + sampleOffset;
}
//----------------------------------------------------------------------------------------------------------------------
template<class SampleType>
double FrameBuilder::FindSyncRisingEdgeEndSamplePos(const std::vector<SampleType>& inputData, const SyncDetector::SyncInfo& syncInfo, double risingEdgePos) const
{
// Find the point at which the leading sync run levels out to an acceptably flat slope
size_t searchPos = (size_t)risingEdgePos;
while ((inputData[searchPos+1] > inputData[searchPos]) && (((double)(inputData[searchPos+1] - inputData[searchPos]) / syncInfo.approxMinMaxSampleRange) > slopeValueFlatTolerance))
{
++searchPos;
}
++searchPos;
return (double)searchPos;
}
//----------------------------------------------------------------------------------------
// Colour burst methods
//----------------------------------------------------------------------------------------
template<class SampleType>
void FrameBuilder::DetectColorBurst(const std::vector<SampleType>& backPorchData, SampleType zeroLevel, SampleType burstAmplitude, std::vector<ColorBurstWaveInfo>& burstWaves) const
{
// Detect all possible burst waves in the sample region
burstWaves.reserve(30);
double runStartPos = 0;
SampleType runPeakSample = backPorchData[0];
bool runIsPositive = (runPeakSample > zeroLevel);
for (size_t i = 1; i < backPorchData.size(); ++i)
{
// If the current sample crosses the zero-level, accept the previous run as a burst wave if it meets the burst
// amplitude, and start a new run from the current sample.
SampleType currentSample = backPorchData[i];
bool currentSampleIsPositive = (currentSample > zeroLevel);
bool crossedZeroLevel = (runIsPositive != currentSampleIsPositive);
if (crossedZeroLevel)
{
// Calculate an approximate position at which the burst wave crossed the zero level, using linear
// interpolation. We already have an upsampled region to examine here which was generated using cubic
// interpolation, so we don't need to go overboard trying to fit the zero level crossing to a curve. Using
// linear interpolation on our upsampled data will be sufficient, especially considering we expect curvature
// to be at a minimum where the sine wave crosses the zero level.
size_t lastSampleNo = i - 1;
SampleType lastSample = backPorchData[lastSampleNo];
double crossZeroLevelPos = (double)(lastSampleNo) + ((double)zeroLevel - (double)lastSample) / ((double)currentSample - (double)lastSample);
// If the previous run reached burst amplitude, add it to the list of detected burst waves.
bool runReachedBurstAmplitude = (runIsPositive ? ((zeroLevel + burstAmplitude) <= runPeakSample) : ((zeroLevel - burstAmplitude) >= runPeakSample));
if (runReachedBurstAmplitude)
{
ColorBurstWaveInfo waveInfo;
waveInfo.isPositive = runIsPositive;
waveInfo.peakLevel = (double)runPeakSample;
waveInfo.startPos = runStartPos;
waveInfo.endPos = crossZeroLevelPos;
waveInfo.isPredicted = false;
burstWaves.push_back(std::move(waveInfo));
}
// Start a new run from the current sample, and advance to the next sample.
runStartPos = crossZeroLevelPos;
runPeakSample = currentSample;
runIsPositive = currentSampleIsPositive;
continue;
}
// Update the peak sample level for this run
runPeakSample = ((runIsPositive ? currentSample > runPeakSample : currentSample < runPeakSample) ? currentSample : runPeakSample);
}
}
//----------------------------------------------------------------------------------------
template<class SampleType>
bool FrameBuilder::RepairColorBurst(const std::vector<SampleType>& backPorchData, SampleType zeroLevel, SampleType burstAmplitude, std::vector<ColorBurstWaveInfo>& burstWaves) const
{
// Trim leading and trailing wave entries where successive waves have the same "polarity". This can happen
// frequently due to noise and other artifacts.
while ((burstWaves.size() > 1) && (burstWaves[0].isPositive == burstWaves[1].isPositive))
{
burstWaves.erase(burstWaves.begin());
}
while ((burstWaves.size() > 1) && (burstWaves[burstWaves.size()-2].isPositive == burstWaves[burstWaves.size()-1].isPositive))
{
burstWaves.resize(burstWaves.size()-1);
}
// If there aren't enough wave entries left to form a full oscillation, return false. We need at least two entries
// in order to perform our repair steps below.
if (burstWaves.size() <= 1)
{
return false;
}
// Build a map of burst wave lengths in samples, snapping close values together. We do this to help determine the
// average wave length below. We don't want to take a simple arithmetic mean however, as we expect bad entries with
// wildly different lengths to be common, and we don't want them skewing the results. We use these numbers to
// essentially "vote" on the average wave length here. Also note that we track separate averages for positive and
// negative burst wave entries. We do this because it's possible our blanking level average is off, possibly from
// noise or other interference, in which case our average sample length may be different for positive and negative
// pulses.
//##TODO## Compare this approach with an arithmetic median calculation.
//##TODO## Consider using the length difference in positive and negative colour burst wave oscillations to improve
//the calculated blanking level
const double colorBurstAverageCalculationTolerance = 0.05;
std::map<double, unsigned int> waveLengthAveragesPositive;
std::map<double, unsigned int> waveLengthAveragesNegative;
for (size_t i = 0; i < burstWaves.size(); ++i)
{
// Start scanning from the middle of the burst wave entries. We do this, because we expect bad wave entries
// to be most frequent on either end of the range, and we want to start building our averages from good
// entries first, since we "snap" length averages to previous entries, and we don't want to seed our
// averages with bad data.
size_t waveIndex = (i + (burstWaves.size() / 2));
waveIndex = (waveIndex >= burstWaves.size() ? waveIndex - burstWaves.size() : waveIndex);
// Build our set of common burst wave lengths
double waveLength = burstWaves[waveIndex].endPos - burstWaves[waveIndex].startPos;
double waveTolerance = waveLength * colorBurstAverageCalculationTolerance;
auto& waveLengthAverages = (burstWaves[waveIndex].isPositive ? waveLengthAveragesPositive : waveLengthAveragesNegative);
auto waveLengthAveragesIterator = waveLengthAverages.begin();
while (waveLengthAveragesIterator != waveLengthAverages.end())
{
if ((waveLength <= (waveLengthAveragesIterator->first + waveTolerance)) && (waveLength >= (waveLengthAveragesIterator->first - waveTolerance)))
{
++waveLengthAveragesIterator->second;
break;
}
++waveLengthAveragesIterator;
}
if (waveLengthAveragesIterator == waveLengthAverages.end())
{
waveLengthAverages[waveLength] = 1;
}
}
// Select the highest occurring wave lengths as the average wave lengths for our positive and negative waves
double waveLengthAveragePositive = 0.0;
unsigned int waveLengthAverageHitCountPositive = 0;
for (auto waveLengthAveragesEntry : waveLengthAveragesPositive)
{
if (waveLengthAveragesEntry.second >= waveLengthAverageHitCountPositive)
{
waveLengthAveragePositive = waveLengthAveragesEntry.first;
waveLengthAverageHitCountPositive = waveLengthAveragesEntry.second;
}
}
double waveLengthAverageNegative = 0.0;
unsigned int waveLengthAverageHitCountNegative = 0;
for (auto waveLengthAveragesEntry : waveLengthAveragesNegative)
{
if (waveLengthAveragesEntry.second >= waveLengthAverageHitCountNegative)
{
waveLengthAverageNegative = waveLengthAveragesEntry.first;
waveLengthAverageHitCountNegative = waveLengthAveragesEntry.second;
}
}
// Perform error correction on the burst waves
double waveLengthAverageTolerancePositive = waveLengthAveragePositive * colorBurstRepairWaveLengthTolerance;
double waveLengthAverageToleranceNegative = waveLengthAverageNegative * colorBurstRepairWaveLengthTolerance;
double waveLengthAverageCombined = (waveLengthAveragePositive + waveLengthAverageNegative) / 2.0;
double waveLengthAverageToleranceCombined = (waveLengthAverageTolerancePositive + waveLengthAverageToleranceNegative) / 2.0;
unsigned int burstWavesIndex = 0;
while (burstWavesIndex < (burstWaves.size() - 1))
{
double waveLengthAverage;
double waveLengthAverageOpposite;
double waveLengthAverageTolerance;
double waveLengthAverageToleranceOpposite;
if (burstWaves[burstWavesIndex].isPositive)
{
waveLengthAverage = waveLengthAveragePositive;
waveLengthAverageOpposite = waveLengthAverageNegative;
waveLengthAverageTolerance = waveLengthAverageTolerancePositive;
waveLengthAverageToleranceOpposite = waveLengthAverageToleranceNegative;
}
else
{
waveLengthAverage = waveLengthAverageNegative;
waveLengthAverageOpposite = waveLengthAveragePositive;
waveLengthAverageTolerance = waveLengthAverageToleranceNegative;
waveLengthAverageToleranceOpposite = waveLengthAverageTolerancePositive;
}
// Calculate the length of the next burst wave, and the length of the gap to the next burst wave. In a valid
// colour burst, there should be no gap between each entry, as we detect each wave start and end position at the
// zero crossing point relative to the blanking level.
double waveLength = burstWaves[burstWavesIndex].endPos - burstWaves[burstWavesIndex].startPos;
double waveGapLength = burstWaves[burstWavesIndex+1].startPos - burstWaves[burstWavesIndex].endPos;
// If a gap is present between this burst wave and the next burst wave, calculate various parameters we'll need
// to assess if we can generate entries to fill the gap.
double followingWaveLength;
double followingWaveLengthAverage;
double followingWaveLengthAverageTolerance;
unsigned int averageWaveGapFillCount;
double minGapLengthForFill;
double maxGapLengthForFill;
if ((waveGapLength > 0) && (burstWavesIndex > 0))
{
followingWaveLength = burstWaves[burstWavesIndex+1].endPos - burstWaves[burstWavesIndex+1].startPos;
followingWaveLengthAverage = (burstWaves[burstWavesIndex+1].isPositive ? waveLengthAveragePositive : waveLengthAverageNegative);
followingWaveLengthAverageTolerance = (burstWaves[burstWavesIndex+1].isPositive ? waveLengthAverageTolerancePositive : waveLengthAverageToleranceNegative);
averageWaveGapFillCount = (unsigned int)((waveGapLength + (waveLengthAverageCombined / 2)) / waveLengthAverageCombined);
minGapLengthForFill = averageWaveGapFillCount * (waveLengthAverageCombined - waveLengthAverageToleranceCombined);
maxGapLengthForFill = averageWaveGapFillCount * (waveLengthAverageCombined + waveLengthAverageToleranceCombined);
}
// Perform error correction on this wave entry
if (!burstWaves[burstWavesIndex].isPredicted && ((waveLength < (waveLengthAverage - waveLengthAverageTolerance)) || (waveLength > (waveLengthAverage + waveLengthAverageTolerance))))
{
// If we haven't accepted any wave entries as valid yet, and this wave entry doesn't fit within our
// tolerance of the average wave length, remove it. Note that we skip this step for predicted wave entries.
// Our predicted waves should generally be within tolerance, but due to round off error they may fall very
// slightly outside the range when created. If we don't exclude them here, we can enter an infinite loop.
burstWaves.erase(burstWaves.begin() + burstWavesIndex);
// If the wave entry we erased wasn't the first one, step back to the previous wave entry so we can
// compare the new next entry with the previous one. If we don't do this and the entry we just erased
// was the second last one for example, we'll abort the loop without even examining the last entry.
if (burstWavesIndex > 0)
{
--burstWavesIndex;
}
continue;
}
else if
(
// A gap is present before the next burst wave, and we're not looking at the first burst wave. We expect bad
// wave entries to be more common at the beginning and end, so we discard initial wave bursts if we
// encounter any gaps.
(waveGapLength > 0)
&& (burstWavesIndex > 0)
// The following wave is within tolerance of its expected length. If this isn't the case, it's possible the
// following wave is "compressing" the gap region, so we can't safely fill in the gap until we remove it.
// It's also likely we're reaching the end of the valid burst waves here, and we're encountering
// questionable wave bursts at the end. In this case, we want to remove these following entries rather than
// try and fill in gaps before them.
&& (followingWaveLength >= (followingWaveLengthAverage - followingWaveLengthAverageTolerance))
&& (followingWaveLength <= (followingWaveLengthAverage + followingWaveLengthAverageTolerance))
&&
(
// We can fit a single wave oscillation in the gap, and the polarity of the resulting oscillations will be consistent.
((waveGapLength >= (waveLengthAverageOpposite - waveLengthAverageToleranceOpposite)) && (waveGapLength <= (waveLengthAverageOpposite + waveLengthAverageToleranceOpposite)) && (burstWaves[burstWavesIndex].isPositive == burstWaves[burstWavesIndex+1].isPositive))
// We can fit multiple wave oscillations in the gap, and the polarity of the resulting oscillations will be consistent.
|| ((waveGapLength >= minGapLengthForFill) && (waveGapLength <= maxGapLengthForFill) && (((averageWaveGapFillCount % 2) != 0) == (burstWaves[burstWavesIndex].isPositive == burstWaves[burstWavesIndex+1].isPositive)))
)
)
{
// If we need to insert a wave entry before the next wave entry in order to close a gap, do it now.
//##TODO## Consider using the separate averages for positive and negative waves to fill the gap. We'll need
// to scale each one by the overall gap length.
double gapFillWaveLength = waveGapLength / (double)averageWaveGapFillCount;
while (waveGapLength > waveLengthAverageToleranceCombined)
{
ColorBurstWaveInfo predictedWaveInfo;
predictedWaveInfo.isPositive = !burstWaves[burstWavesIndex].isPositive;
predictedWaveInfo.startPos = burstWaves[burstWavesIndex].endPos;
predictedWaveInfo.endPos = predictedWaveInfo.startPos + gapFillWaveLength;
predictedWaveInfo.endPos = (predictedWaveInfo.endPos > burstWaves[burstWavesIndex+1].startPos) ? burstWaves[burstWavesIndex+1].startPos : predictedWaveInfo.endPos;
predictedWaveInfo.peakLevel = burstWaves[burstWavesIndex].peakLevel;
predictedWaveInfo.isPredicted = true;
burstWaves.insert(burstWaves.begin() + (burstWavesIndex + 1), predictedWaveInfo);
++burstWavesIndex;
waveGapLength = burstWaves[burstWavesIndex+1].startPos - burstWaves[burstWavesIndex].endPos;
}
// If we've left a small gap to the next wave entry, close it now. This could occur due to round off error.
if (waveGapLength > 0)
{
burstWaves[burstWavesIndex].endPos = burstWaves[burstWavesIndex+1].startPos;
}
continue;
}
else if ((waveGapLength > 0) || (burstWaves[burstWavesIndex].isPositive == burstWaves[burstWavesIndex+1].isPositive))
{
// If there's a gap to the next wave entry but it's not large enough to fit a valid wave oscillation into,
// or if we don't have differing polarity of two consecutive wave entries, we need to erase either the
// current wave entry or following wave entry. If we haven't found at least two good consecutive burst waves
// (one full oscillation) yet, we assume the leading sample is wrong and that's why we have a gap. In this
// case, we erase the current wave entry, otherwise we assume the following sample is wrong, in which case
// we erase the following wave sample.
if (burstWavesIndex < 2)
{
// Erase the current burst wave entry
burstWaves.erase(burstWaves.begin() + burstWavesIndex);
// If the wave entry we erased wasn't the first one, step back to the previous wave entry so we can
// compare the new next entry with the previous one. If we don't do this and the entry we just erased
// was the second last one for example, we'll abort the loop without even examining the last entry.
if (burstWavesIndex > 0)
{
--burstWavesIndex;
}
}
else
{
// Erase the following burst wave entry
burstWaves.erase(burstWaves.begin() + (burstWavesIndex + 1));
}
continue;
}
// Advance to the next wave entry
++burstWavesIndex;
}
// Attempt to validate the color burst after the repair operation, and return the result the caller.
bool colorBurstValid = ValidateColorBurst(colorBurstMinimumHalfOscillationCount, burstWaves);
return colorBurstValid;
}
//----------------------------------------------------------------------------------------
// IRE conversion methods
//----------------------------------------------------------------------------------------
template<class SampleType>
float FrameBuilder::SampleToIRE(SampleType sampleValue, float ireLevel0, float ireLevel100) const
{
return ((float)sampleValue - ireLevel0) * (100.0f / (ireLevel100 - ireLevel0));
}
//----------------------------------------------------------------------------------------
template<class SampleType>
SampleType FrameBuilder::IREToSample(float ire, float ireLevel0, float ireLevel100) const
{
return (SampleType)(((ire * ((ireLevel100 - ireLevel0) / 100.0f)) + ireLevel0) + (std::numeric_limits<SampleType>::is_integer ? 0.5f : 0.0f));
}