1 // license:BSD-3-Clause
2 // copyright-holders:Aaron Giles
3 /***************************************************************************
4 
5     ldresample.c
6 
7     Laserdisc audio synchronizer and resampler.
8 
9 ****************************************************************************/
10 
11 #include <cstdio>
12 #include <cctype>
13 #include <cstdlib>
14 #include <cmath>
15 #include <new>
16 #include <cassert>
17 #include "bitmap.h"
18 #include "chd.h"
19 #include "avhuff.h"
20 #include "vbiparse.h"
21 
22 
23 
24 //**************************************************************************
25 //  CONSTANTS
26 //**************************************************************************
27 
28 // size of window where we scan ahead to find maximum; this should be large enough to
29 // catch peaks of even slow waves
30 const uint32_t MAXIMUM_WINDOW_SIZE = 40;
31 
32 // number of standard deviations away from silence that we consider a real signal
33 const uint32_t SIGNAL_DEVIATIONS = 100;
34 
35 // number of standard deviations away from silence that we consider the start of a signal
36 const uint32_t SIGNAL_START_DEVIATIONS = 5;
37 
38 // number of consecutive entries of signal before we consider that we found it
39 const uint32_t MINIMUM_SIGNAL_COUNT = 20;
40 
41 
42 
43 //**************************************************************************
44 //  TYPE DEFINITIONS
45 //**************************************************************************
46 
47 struct movie_info
48 {
49 	double          framerate;
50 	int             iframerate;
51 	int             numfields;
52 	int             width;
53 	int             height;
54 	int             samplerate;
55 	int             channels;
56 	int             interlaced;
57 	bitmap_yuy16    bitmap;
58 	std::vector<int16_t>   lsound;
59 	std::vector<int16_t>   rsound;
60 	uint32_t          samples;
61 };
62 
63 
64 // ======================> chd_resample_compressor
65 
66 class chd_resample_compressor : public chd_file_compressor
67 {
68 public:
69 	// construction/destruction
chd_resample_compressor(chd_file & source,movie_info & info,int64_t ioffset,int64_t islope)70 	chd_resample_compressor(chd_file &source, movie_info &info, int64_t ioffset, int64_t islope)
71 		: m_source(source),
72 			m_info(info),
73 			m_ioffset(ioffset),
74 			m_islope(islope) { }
75 
76 	// read interface
read_data(void * _dest,uint64_t offset,uint32_t length)77 	virtual uint32_t read_data(void *_dest, uint64_t offset, uint32_t length)
78 	{
79 		assert(offset % m_source.hunk_bytes() == 0);
80 		assert(length % m_source.hunk_bytes() == 0);
81 
82 		uint32_t startfield = offset / m_source.hunk_bytes();
83 		uint32_t endfield = startfield + length / m_source.hunk_bytes();
84 		uint8_t *dest = reinterpret_cast<uint8_t *>(_dest);
85 
86 		for (uint32_t fieldnum = startfield; fieldnum < endfield; fieldnum++)
87 		{
88 			generate_one_frame(dest, m_source.hunk_bytes(), fieldnum);
89 			dest += m_source.hunk_bytes();
90 		}
91 		return length;
92 	}
93 
94 private:
95 	// internal helpers
96 	void generate_one_frame(uint8_t *dest, uint32_t datasize, uint32_t fieldnum);
97 
98 	// internal state
99 	chd_file &                  m_source;
100 	movie_info &                m_info;
101 	int64_t                       m_ioffset;
102 	int64_t                       m_islope;
103 };
104 
105 
106 
107 //**************************************************************************
108 //  INLINE FUNCTIONS
109 //**************************************************************************
110 
111 //-------------------------------------------------
112 //  field_to_sample_number - given a field number
113 //  compute the absolute sample number for the
114 //  first sample of that field
115 //-------------------------------------------------
116 
field_to_sample_number(const movie_info & info,uint32_t field)117 inline uint32_t field_to_sample_number(const movie_info &info, uint32_t field)
118 {
119 	return (uint64_t(info.samplerate) * uint64_t(field) * uint64_t(1000000) + info.iframerate - 1) / uint64_t(info.iframerate);
120 }
121 
122 
123 //-------------------------------------------------
124 //  sample_number_to_field - given a sample number
125 //  compute the field where it is located and
126 //  the offset within the field
127 //-------------------------------------------------
128 
sample_number_to_field(const movie_info & info,uint32_t samplenum,uint32_t & offset)129 inline uint32_t sample_number_to_field(const movie_info &info, uint32_t samplenum, uint32_t &offset)
130 {
131 	uint32_t guess = (uint64_t(samplenum) * uint64_t(info.iframerate) + (uint64_t(info.samplerate) * uint64_t(1000000) - 1)) / (uint64_t(info.samplerate) * uint64_t(1000000));
132 	while (1)
133 	{
134 		uint32_t fieldstart = field_to_sample_number(info, guess);
135 		uint32_t fieldend = field_to_sample_number(info, guess + 1);
136 		if (samplenum >= fieldstart && samplenum < fieldend)
137 		{
138 			offset = samplenum - fieldstart;
139 			return guess;
140 		}
141 		else if (samplenum < fieldstart)
142 			guess--;
143 		else
144 			guess++;
145 	}
146 }
147 
148 
149 
150 //**************************************************************************
151 //  CHD HANDLING
152 //**************************************************************************
153 
154 //-------------------------------------------------
155 //  open_chd - open a CHD file and return
156 //  information about it
157 //-------------------------------------------------
158 
open_chd(chd_file & file,const char * filename,movie_info & info)159 static chd_error open_chd(chd_file &file, const char *filename, movie_info &info)
160 {
161 	// open the file
162 	chd_error chderr = file.open(filename);
163 	if (chderr != CHDERR_NONE)
164 	{
165 		fprintf(stderr, "Error opening CHD file: %s\n", chd_file::error_string(chderr));
166 		return chderr;
167 	}
168 
169 	// get the metadata
170 	std::string metadata;
171 	chderr = file.read_metadata(AV_METADATA_TAG, 0, metadata);
172 	if (chderr != CHDERR_NONE)
173 	{
174 		fprintf(stderr, "Error getting A/V metadata: %s\n", chd_file::error_string(chderr));
175 		return chderr;
176 	}
177 
178 	// extract the info
179 	int fps, fpsfrac, width, height, interlaced, channels, rate;
180 	if (sscanf(metadata.c_str(), AV_METADATA_FORMAT, &fps, &fpsfrac, &width, &height, &interlaced, &channels, &rate) != 7)
181 	{
182 		fprintf(stderr, "Improperly formatted metadata\n");
183 		return CHDERR_INVALID_DATA;
184 	}
185 
186 	// extract movie info
187 	info.iframerate = fps * 1000000 + fpsfrac;
188 	info.framerate = info.iframerate / 1000000.0;
189 	info.numfields = file.hunk_count();
190 	info.width = width;
191 	info.height = height;
192 	info.interlaced = interlaced;
193 	info.samplerate = rate;
194 	info.channels = channels;
195 
196 	// allocate buffers
197 	info.bitmap.resize(info.width, info.height);
198 	info.lsound.resize(info.samplerate);
199 	info.rsound.resize(info.samplerate);
200 	return CHDERR_NONE;
201 }
202 
203 
204 //-------------------------------------------------
205 //  create_chd - create a new CHD file
206 //-------------------------------------------------
207 
create_chd(chd_file_compressor & file,const char * filename,chd_file & source,const movie_info & info)208 static chd_error create_chd(chd_file_compressor &file, const char *filename, chd_file &source, const movie_info &info)
209 {
210 	// create the file
211 	chd_codec_type compression[4] = { CHD_CODEC_AVHUFF };
212 	chd_error chderr = file.create(filename, source.logical_bytes(), source.hunk_bytes(), source.unit_bytes(), compression);
213 	if (chderr != CHDERR_NONE)
214 	{
215 		fprintf(stderr, "Error creating new CHD file: %s\n", chd_file::error_string(chderr));
216 		return chderr;
217 	}
218 
219 	// clone the metadata
220 	chderr = file.clone_all_metadata(source);
221 	if (chderr != CHDERR_NONE)
222 	{
223 		fprintf(stderr, "Error cloning metadata: %s\n", chd_file::error_string(chderr));
224 		return chderr;
225 	}
226 
227 	// begin compressing
228 	file.compress_begin();
229 	return CHDERR_NONE;
230 }
231 
232 
233 //-------------------------------------------------
234 //  read_chd - read a field from a CHD file
235 //-------------------------------------------------
236 
read_chd(chd_file & file,uint32_t field,movie_info & info,uint32_t soundoffs)237 static bool read_chd(chd_file &file, uint32_t field, movie_info &info, uint32_t soundoffs)
238 {
239 	// configure the codec
240 	avhuff_decoder::config avconfig;
241 	avconfig.video = &info.bitmap;
242 	avconfig.maxsamples = info.lsound.size();
243 	avconfig.actsamples = &info.samples;
244 	avconfig.audio[0] = &info.lsound[soundoffs];
245 	avconfig.audio[1] = &info.rsound[soundoffs];
246 
247 	// configure the decompressor for this field
248 	file.codec_configure(CHD_CODEC_AVHUFF, AVHUFF_CODEC_DECOMPRESS_CONFIG, &avconfig);
249 
250 	// read the field
251 	chd_error chderr = file.read_hunk(field, nullptr);
252 	return (chderr == CHDERR_NONE);
253 }
254 
255 
256 
257 //**************************************************************************
258 //  CORE IMPLEMENTATION
259 //**************************************************************************
260 
261 //-------------------------------------------------
262 //  find_edge_near_field - given a field number,
263 //  load +/- 1/2 second on either side and find
264 //  an audio edge
265 //-------------------------------------------------
266 
find_edge_near_field(chd_file & srcfile,uint32_t fieldnum,movie_info & info,bool report_best_field,int32_t & delta)267 static bool find_edge_near_field(chd_file &srcfile, uint32_t fieldnum, movie_info &info, bool report_best_field, int32_t &delta)
268 {
269 	// clear the sound buffers
270 	memset(&info.lsound[0], 0, info.lsound.size() * 2);
271 	memset(&info.rsound[0], 0, info.rsound.size() * 2);
272 
273 	// read 1 second around the target area
274 	int fields_to_read = info.iframerate / 1000000;
275 	int32_t firstfield = fieldnum - (fields_to_read / 2);
276 	uint32_t targetsoundstart = 0;
277 	uint32_t firstfieldend = 0;
278 	uint32_t fieldstart[100];
279 	uint32_t soundend = 0;
280 	for (int32_t curfield = 0; curfield < fields_to_read; curfield++)
281 	{
282 		// remember the start of each field
283 		fieldstart[curfield] = soundend;
284 
285 		// remember the sound offset where the initial fieldnum is
286 		if (firstfield + curfield == fieldnum)
287 			targetsoundstart = soundend;
288 
289 		// read the frame and samples
290 		if (firstfield + curfield >= 0)
291 		{
292 			read_chd(srcfile, firstfield + curfield, info, soundend);
293 			soundend += info.samples;
294 
295 			// also remember the offset at the end of the first field
296 			if (firstfieldend == 0)
297 				firstfieldend = soundend;
298 		}
299 	}
300 
301 	// compute absolute deltas across the samples
302 	for (uint32_t sampnum = 0; sampnum < soundend; sampnum++)
303 	{
304 		info.lsound[sampnum] = labs(info.lsound[sampnum + 1] - info.lsound[sampnum]);
305 		info.rsound[sampnum] = labs(info.rsound[sampnum + 1] - info.rsound[sampnum]);
306 	}
307 
308 	// for each sample in the collection, find the highest deltas over the
309 	// next few samples, and take the nth highest value (to remove outliers)
310 	for (uint32_t sampnum = 0; sampnum < soundend - MAXIMUM_WINDOW_SIZE; sampnum++)
311 	{
312 		// scan forward over the maximum window
313 		uint32_t lmax = 0, rmax = 0;
314 		for (uint32_t scannum = 0; scannum < MAXIMUM_WINDOW_SIZE; scannum++)
315 		{
316 			if (info.lsound[sampnum + scannum] > lmax)
317 				lmax = info.lsound[sampnum + scannum];
318 			if (info.rsound[sampnum + scannum] > rmax)
319 				rmax = info.rsound[sampnum + scannum];
320 		}
321 
322 		// replace this sample with the maximum value found
323 		info.lsound[sampnum] = lmax;
324 		info.rsound[sampnum] = rmax;
325 	}
326 
327 	// now compute the average over the first field, which is assumed to be silence
328 	uint32_t firstlavg = 0;
329 	uint32_t firstravg = 0;
330 	for (uint32_t sampnum = 0; sampnum < firstfieldend; sampnum++)
331 	{
332 		firstlavg += info.lsound[sampnum];
333 		firstravg += info.rsound[sampnum];
334 	}
335 	firstlavg /= firstfieldend;
336 	firstravg /= firstfieldend;
337 
338 	// then compute the standard deviation over the first field
339 	uint32_t firstldev = 0;
340 	uint32_t firstrdev = 0;
341 	for (uint32_t sampnum = 0; sampnum < firstfieldend; sampnum++)
342 	{
343 		firstldev += (info.lsound[sampnum] - firstlavg) * (info.lsound[sampnum] - firstlavg);
344 		firstrdev += (info.rsound[sampnum] - firstravg) * (info.rsound[sampnum] - firstravg);
345 	}
346 	firstldev = sqrt(double(firstldev) / firstfieldend);
347 	firstrdev = sqrt(double(firstrdev) / firstfieldend);
348 
349 	// scan forward through the samples, counting consecutive samples more than
350 	// SIGNAL_DEVIATIONS standard deviations away from silence
351 	uint32_t lcount = 0;
352 	uint32_t rcount = 0;
353 	uint32_t sampnum = 0;
354 	for (sampnum = 0; sampnum < soundend; sampnum++)
355 	{
356 		// left speaker
357 		if (info.lsound[sampnum] > firstlavg + SIGNAL_DEVIATIONS * firstldev)
358 			lcount++;
359 		else
360 			lcount = 0;
361 
362 		// right speaker
363 		if (info.rsound[sampnum] > firstravg + SIGNAL_DEVIATIONS * firstrdev)
364 			rcount++;
365 		else
366 			rcount = 0;
367 
368 		// stop if we find enough
369 		if (lcount > MINIMUM_SIGNAL_COUNT || rcount > MINIMUM_SIGNAL_COUNT)
370 			break;
371 	}
372 
373 	// if we didn't find any, return failure
374 	if (sampnum >= soundend)
375 	{
376 		if (!report_best_field)
377 			printf("Field %5d: Unable to find edge\n", fieldnum);
378 		return false;
379 	}
380 
381 	// scan backwards to find the start of the signal
382 	for ( ; sampnum > 0; sampnum--)
383 		if (info.lsound[sampnum - 1] < firstlavg + SIGNAL_START_DEVIATIONS * firstldev ||
384 			info.rsound[sampnum - 1] < firstravg + SIGNAL_START_DEVIATIONS * firstrdev)
385 			break;
386 
387 	// if we're to report the best field, figure out which field we are in
388 	if (report_best_field)
389 	{
390 		int32_t curfield;
391 		for (curfield = 0; curfield < fields_to_read - 1; curfield++)
392 			if (sampnum < fieldstart[curfield + 1])
393 				break;
394 		printf("Field %5d: Edge found at offset %d (frame %.1f)\n", firstfield + curfield, sampnum - fieldstart[curfield], (double)(firstfield + curfield) * 0.5);
395 	}
396 
397 	// otherwise, compute the delta from the provided field number
398 	else
399 	{
400 		printf("Field %5d: Edge at offset %d from expected (found at %d, expected %d)\n", fieldnum, sampnum - targetsoundstart, sampnum, targetsoundstart);
401 		delta = sampnum - targetsoundstart;
402 	}
403 	return true;
404 }
405 
406 
407 //-------------------------------------------------
408 //  generate_one_frame - generate a single
409 //  resampled frame
410 //-------------------------------------------------
411 
generate_one_frame(uint8_t * dest,uint32_t datasize,uint32_t fieldnum)412 void chd_resample_compressor::generate_one_frame(uint8_t *dest, uint32_t datasize, uint32_t fieldnum)
413 {
414 	// determine the first field needed to cover this range of samples
415 	uint32_t srcbegin = field_to_sample_number(m_info, fieldnum);
416 	int64_t dstbegin = (int64_t(srcbegin) << 24) + m_ioffset + m_islope * fieldnum;
417 	uint32_t dstbeginoffset;
418 	int32_t dstbeginfield;
419 	if (dstbegin >= 0)
420 		dstbeginfield = sample_number_to_field(m_info, dstbegin >> 24, dstbeginoffset);
421 	else
422 	{
423 		dstbeginfield = -1 - sample_number_to_field(m_info, -dstbegin >> 24, dstbeginoffset);
424 		dstbeginoffset = (field_to_sample_number(m_info, -dstbeginfield) - field_to_sample_number(m_info, -dstbeginfield - 1)) - dstbeginoffset;
425 	}
426 
427 	// determine the last field needed to cover this range of samples
428 	uint32_t srcend = field_to_sample_number(m_info, fieldnum + 1);
429 	int64_t dstend = (int64_t(srcend) << 24) + m_ioffset + m_islope * (fieldnum + 1);
430 	uint32_t dstendoffset;
431 	int32_t dstendfield;
432 	if (dstend >= 0)
433 		dstendfield = sample_number_to_field(m_info, dstend >> 24, dstendoffset);
434 	else
435 	{
436 		dstendfield = -1 - -sample_number_to_field(m_info, -dstend >> 24, dstendoffset);
437 		dstendoffset = (field_to_sample_number(m_info, -dstendfield) - field_to_sample_number(m_info, -dstendfield - 1)) - dstendoffset;
438 	}
439 /*
440 printf("%5d: start=%10d (%5d.%03d) end=%10d (%5d.%03d)\n",
441         fieldnum,
442         (int32_t)(dstbegin >> 24), dstbeginfield, dstbeginoffset,
443         (int32_t)(dstend >> 24), dstendfield, dstendoffset);
444 */
445 	// read all samples required into the end of the sound buffers
446 	uint32_t dstoffset = srcend - srcbegin;
447 	for (int32_t dstfield = dstbeginfield; dstfield <= dstendfield; dstfield++)
448 	{
449 		if (dstfield >= 0)
450 			read_chd(m_source, dstfield, m_info, dstoffset);
451 		else
452 		{
453 			m_info.samples = field_to_sample_number(m_info, -dstfield) - field_to_sample_number(m_info, -dstfield - 1);
454 			memset(&m_info.lsound[dstoffset], 0, m_info.samples * sizeof(m_info.lsound[0]));
455 			memset(&m_info.rsound[dstoffset], 0, m_info.samples * sizeof(m_info.rsound[0]));
456 		}
457 		dstoffset += m_info.samples;
458 	}
459 
460 	// resample the destination samples to the source
461 	dstoffset = srcend - srcbegin;
462 	int64_t dstpos = dstbegin;
463 	int64_t dststep = (dstend - dstbegin) / int64_t(srcend - srcbegin);
464 	for (uint32_t srcoffset = 0; srcoffset < srcend - srcbegin; srcoffset++)
465 	{
466 		m_info.lsound[srcoffset] = m_info.lsound[(int)(dstoffset + dstbeginoffset + (dstpos >> 24) - (dstbegin >> 24))];
467 		m_info.rsound[srcoffset] = m_info.rsound[(int)(dstoffset + dstbeginoffset + (dstpos >> 24) - (dstbegin >> 24))];
468 		dstpos += dststep;
469 	}
470 
471 	// read the original frame, pointing the sound buffer past where we've calculated
472 	read_chd(m_source, fieldnum, m_info, srcend - srcbegin);
473 
474 	// assemble the final frame
475 	std::vector<uint8_t> buffer;
476 	int16_t *sampledata[2] = { &m_info.lsound[0], &m_info.rsound[0] };
477 	avhuff_encoder::assemble_data(buffer, m_info.bitmap, m_info.channels, m_info.samples, sampledata);
478 	memcpy(dest, &buffer[0], std::min(buffer.size(), size_t(datasize)));
479 	if (buffer.size() < datasize)
480 		memset(&dest[buffer.size()], 0, datasize - buffer.size());
481 }
482 
483 
484 //-------------------------------------------------
485 //  usage - display program usage
486 //-------------------------------------------------
487 
usage(void)488 static int usage(void)
489 {
490 	fprintf(stderr, "Usage: \n");
491 	fprintf(stderr, "  ldresample source.chd\n");
492 	fprintf(stderr, "  ldresample source.chd output.chd offset [slope]\n");
493 	fprintf(stderr, "\n");
494 	fprintf(stderr, "Where offset and slope make a linear equation f(x) which\n");
495 	fprintf(stderr, "describes the sample offset from the source as a function\n");
496 	fprintf(stderr, "of field number.\n");
497 	return 1;
498 }
499 
500 
501 //-------------------------------------------------
502 //  main - main entry point
503 //-------------------------------------------------
504 
main(int argc,char * argv[])505 int main(int argc, char *argv[])
506 {
507 	// verify arguments
508 	if (argc < 2)
509 		return usage();
510 	const char *srcfilename = argv[1];
511 	const char *dstfilename = (argc < 3) ? nullptr : argv[2];
512 	double offset = (argc < 4) ? 0.0 : atof(argv[3]);
513 	double slope = (argc < 5) ? 1.0 : atof(argv[4]);
514 
515 	// print basic information
516 	printf("Input file: %s\n", srcfilename);
517 	if (dstfilename != nullptr)
518 	{
519 		printf("Output file: %s\n", dstfilename);
520 		printf("Offset: %f\n", offset);
521 		printf("Slope: %f\n", slope);
522 	}
523 
524 	// open the source file
525 	chd_file srcfile;
526 	movie_info info;
527 	chd_error err = open_chd(srcfile, srcfilename, info);
528 	if (err != CHDERR_NONE)
529 	{
530 		fprintf(stderr, "Unable to open file '%s'\n", srcfilename);
531 		return 1;
532 	}
533 
534 	// output some basics
535 	printf("Video dimensions: %dx%d\n", info.width, info.height);
536 	printf("Video frame rate: %.2fHz\n", info.framerate);
537 	printf("Sample rate: %dHz\n", info.samplerate);
538 	printf("Total fields: %d\n", info.numfields);
539 
540 	// if we don't have a destination file, scan for edges
541 	if (dstfilename == nullptr)
542 	{
543 		for (uint32_t fieldnum = 60; fieldnum < info.numfields - 60; fieldnum += 30)
544 		{
545 			fprintf(stderr, "Field %5d\r", fieldnum);
546 			int32_t delta;
547 			find_edge_near_field(srcfile, fieldnum, info, true, delta);
548 		}
549 	}
550 
551 	// otherwise, resample the source to the destination
552 	else
553 	{
554 		// open the destination file
555 		chd_resample_compressor dstfile(srcfile, info, int64_t(offset * 65536.0 * 256.0), int64_t(slope * 65536.0 * 256.0));
556 		err = create_chd(dstfile, dstfilename, srcfile, info);
557 		if (!dstfile.opened())
558 		{
559 			fprintf(stderr, "Unable to create file '%s'\n", dstfilename);
560 			return 1;
561 		}
562 
563 		// loop over all the fields in the source file
564 		double progress, ratio;
565 		osd_ticks_t last_update = 0;
566 		while (dstfile.compress_continue(progress, ratio) == CHDERR_COMPRESSING)
567 			if (osd_ticks() - last_update > osd_ticks_per_second() / 4)
568 			{
569 				last_update = osd_ticks();
570 				printf("Processing, %.1f%% complete....\r", progress * 100.0);
571 			}
572 	}
573 	return 0;
574 }
575