// license:BSD-3-Clause // copyright-holders:Aaron Giles /*************************************************************************** ldresample.c Laserdisc audio synchronizer and resampler. ****************************************************************************/ #include #include #include #include #include #include #include "bitmap.h" #include "chd.h" #include "avhuff.h" #include "vbiparse.h" //************************************************************************** // CONSTANTS //************************************************************************** // size of window where we scan ahead to find maximum; this should be large enough to // catch peaks of even slow waves const uint32_t MAXIMUM_WINDOW_SIZE = 40; // number of standard deviations away from silence that we consider a real signal const uint32_t SIGNAL_DEVIATIONS = 100; // number of standard deviations away from silence that we consider the start of a signal const uint32_t SIGNAL_START_DEVIATIONS = 5; // number of consecutive entries of signal before we consider that we found it const uint32_t MINIMUM_SIGNAL_COUNT = 20; //************************************************************************** // TYPE DEFINITIONS //************************************************************************** struct movie_info { double framerate; int iframerate; int numfields; int width; int height; int samplerate; int channels; int interlaced; bitmap_yuy16 bitmap; std::vector lsound; std::vector rsound; uint32_t samples; }; // ======================> chd_resample_compressor class chd_resample_compressor : public chd_file_compressor { public: // construction/destruction chd_resample_compressor(chd_file &source, movie_info &info, int64_t ioffset, int64_t islope) : m_source(source), m_info(info), m_ioffset(ioffset), m_islope(islope) { } // read interface virtual uint32_t read_data(void *_dest, uint64_t offset, uint32_t length) { assert(offset % m_source.hunk_bytes() == 0); assert(length % m_source.hunk_bytes() == 0); uint32_t startfield = offset / m_source.hunk_bytes(); uint32_t endfield = startfield + length / m_source.hunk_bytes(); uint8_t *dest = reinterpret_cast(_dest); for (uint32_t fieldnum = startfield; fieldnum < endfield; fieldnum++) { generate_one_frame(dest, m_source.hunk_bytes(), fieldnum); dest += m_source.hunk_bytes(); } return length; } private: // internal helpers void generate_one_frame(uint8_t *dest, uint32_t datasize, uint32_t fieldnum); // internal state chd_file & m_source; movie_info & m_info; int64_t m_ioffset; int64_t m_islope; }; //************************************************************************** // INLINE FUNCTIONS //************************************************************************** //------------------------------------------------- // field_to_sample_number - given a field number // compute the absolute sample number for the // first sample of that field //------------------------------------------------- inline uint32_t field_to_sample_number(const movie_info &info, uint32_t field) { return (uint64_t(info.samplerate) * uint64_t(field) * uint64_t(1000000) + info.iframerate - 1) / uint64_t(info.iframerate); } //------------------------------------------------- // sample_number_to_field - given a sample number // compute the field where it is located and // the offset within the field //------------------------------------------------- inline uint32_t sample_number_to_field(const movie_info &info, uint32_t samplenum, uint32_t &offset) { 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)); while (1) { uint32_t fieldstart = field_to_sample_number(info, guess); uint32_t fieldend = field_to_sample_number(info, guess + 1); if (samplenum >= fieldstart && samplenum < fieldend) { offset = samplenum - fieldstart; return guess; } else if (samplenum < fieldstart) guess--; else guess++; } } //************************************************************************** // CHD HANDLING //************************************************************************** //------------------------------------------------- // open_chd - open a CHD file and return // information about it //------------------------------------------------- static chd_error open_chd(chd_file &file, const char *filename, movie_info &info) { // open the file chd_error chderr = file.open(filename); if (chderr != CHDERR_NONE) { fprintf(stderr, "Error opening CHD file: %s\n", chd_file::error_string(chderr)); return chderr; } // get the metadata std::string metadata; chderr = file.read_metadata(AV_METADATA_TAG, 0, metadata); if (chderr != CHDERR_NONE) { fprintf(stderr, "Error getting A/V metadata: %s\n", chd_file::error_string(chderr)); return chderr; } // extract the info int fps, fpsfrac, width, height, interlaced, channels, rate; if (sscanf(metadata.c_str(), AV_METADATA_FORMAT, &fps, &fpsfrac, &width, &height, &interlaced, &channels, &rate) != 7) { fprintf(stderr, "Improperly formatted metadata\n"); return CHDERR_INVALID_DATA; } // extract movie info info.iframerate = fps * 1000000 + fpsfrac; info.framerate = info.iframerate / 1000000.0; info.numfields = file.hunk_count(); info.width = width; info.height = height; info.interlaced = interlaced; info.samplerate = rate; info.channels = channels; // allocate buffers info.bitmap.resize(info.width, info.height); info.lsound.resize(info.samplerate); info.rsound.resize(info.samplerate); return CHDERR_NONE; } //------------------------------------------------- // create_chd - create a new CHD file //------------------------------------------------- static chd_error create_chd(chd_file_compressor &file, const char *filename, chd_file &source, const movie_info &info) { // create the file chd_codec_type compression[4] = { CHD_CODEC_AVHUFF }; chd_error chderr = file.create(filename, source.logical_bytes(), source.hunk_bytes(), source.unit_bytes(), compression); if (chderr != CHDERR_NONE) { fprintf(stderr, "Error creating new CHD file: %s\n", chd_file::error_string(chderr)); return chderr; } // clone the metadata chderr = file.clone_all_metadata(source); if (chderr != CHDERR_NONE) { fprintf(stderr, "Error cloning metadata: %s\n", chd_file::error_string(chderr)); return chderr; } // begin compressing file.compress_begin(); return CHDERR_NONE; } //------------------------------------------------- // read_chd - read a field from a CHD file //------------------------------------------------- static bool read_chd(chd_file &file, uint32_t field, movie_info &info, uint32_t soundoffs) { // configure the codec avhuff_decompress_config avconfig; avconfig.video.wrap(info.bitmap, info.bitmap.cliprect()); avconfig.maxsamples = info.lsound.size(); avconfig.actsamples = &info.samples; avconfig.audio[0] = &info.lsound[soundoffs]; avconfig.audio[1] = &info.rsound[soundoffs]; // configure the decompressor for this field file.codec_configure(CHD_CODEC_AVHUFF, AVHUFF_CODEC_DECOMPRESS_CONFIG, &avconfig); // read the field chd_error chderr = file.read_hunk(field, nullptr); return (chderr == CHDERR_NONE); } //************************************************************************** // CORE IMPLEMENTATION //************************************************************************** //------------------------------------------------- // find_edge_near_field - given a field number, // load +/- 1/2 second on either side and find // an audio edge //------------------------------------------------- static bool find_edge_near_field(chd_file &srcfile, uint32_t fieldnum, movie_info &info, bool report_best_field, int32_t &delta) { // clear the sound buffers memset(&info.lsound[0], 0, info.lsound.size() * 2); memset(&info.rsound[0], 0, info.rsound.size() * 2); // read 1 second around the target area int fields_to_read = info.iframerate / 1000000; int32_t firstfield = fieldnum - (fields_to_read / 2); uint32_t targetsoundstart = 0; uint32_t firstfieldend = 0; uint32_t fieldstart[100]; uint32_t soundend = 0; for (int32_t curfield = 0; curfield < fields_to_read; curfield++) { // remember the start of each field fieldstart[curfield] = soundend; // remember the sound offset where the initial fieldnum is if (firstfield + curfield == fieldnum) targetsoundstart = soundend; // read the frame and samples if (firstfield + curfield >= 0) { read_chd(srcfile, firstfield + curfield, info, soundend); soundend += info.samples; // also remember the offset at the end of the first field if (firstfieldend == 0) firstfieldend = soundend; } } // compute absolute deltas across the samples for (uint32_t sampnum = 0; sampnum < soundend; sampnum++) { info.lsound[sampnum] = labs(info.lsound[sampnum + 1] - info.lsound[sampnum]); info.rsound[sampnum] = labs(info.rsound[sampnum + 1] - info.rsound[sampnum]); } // for each sample in the collection, find the highest deltas over the // next few samples, and take the nth highest value (to remove outliers) for (uint32_t sampnum = 0; sampnum < soundend - MAXIMUM_WINDOW_SIZE; sampnum++) { // scan forward over the maximum window uint32_t lmax = 0, rmax = 0; for (uint32_t scannum = 0; scannum < MAXIMUM_WINDOW_SIZE; scannum++) { if (info.lsound[sampnum + scannum] > lmax) lmax = info.lsound[sampnum + scannum]; if (info.rsound[sampnum + scannum] > rmax) rmax = info.rsound[sampnum + scannum]; } // replace this sample with the maximum value found info.lsound[sampnum] = lmax; info.rsound[sampnum] = rmax; } // now compute the average over the first field, which is assumed to be silence uint32_t firstlavg = 0; uint32_t firstravg = 0; for (uint32_t sampnum = 0; sampnum < firstfieldend; sampnum++) { firstlavg += info.lsound[sampnum]; firstravg += info.rsound[sampnum]; } firstlavg /= firstfieldend; firstravg /= firstfieldend; // then compute the standard deviation over the first field uint32_t firstldev = 0; uint32_t firstrdev = 0; for (uint32_t sampnum = 0; sampnum < firstfieldend; sampnum++) { firstldev += (info.lsound[sampnum] - firstlavg) * (info.lsound[sampnum] - firstlavg); firstrdev += (info.rsound[sampnum] - firstravg) * (info.rsound[sampnum] - firstravg); } firstldev = sqrt(double(firstldev) / firstfieldend); firstrdev = sqrt(double(firstrdev) / firstfieldend); // scan forward through the samples, counting consecutive samples more than // SIGNAL_DEVIATIONS standard deviations away from silence uint32_t lcount = 0; uint32_t rcount = 0; uint32_t sampnum = 0; for (sampnum = 0; sampnum < soundend; sampnum++) { // left speaker if (info.lsound[sampnum] > firstlavg + SIGNAL_DEVIATIONS * firstldev) lcount++; else lcount = 0; // right speaker if (info.rsound[sampnum] > firstravg + SIGNAL_DEVIATIONS * firstrdev) rcount++; else rcount = 0; // stop if we find enough if (lcount > MINIMUM_SIGNAL_COUNT || rcount > MINIMUM_SIGNAL_COUNT) break; } // if we didn't find any, return failure if (sampnum >= soundend) { if (!report_best_field) printf("Field %5d: Unable to find edge\n", fieldnum); return false; } // scan backwards to find the start of the signal for ( ; sampnum > 0; sampnum--) if (info.lsound[sampnum - 1] < firstlavg + SIGNAL_START_DEVIATIONS * firstldev || info.rsound[sampnum - 1] < firstravg + SIGNAL_START_DEVIATIONS * firstrdev) break; // if we're to report the best field, figure out which field we are in if (report_best_field) { int32_t curfield; for (curfield = 0; curfield < fields_to_read - 1; curfield++) if (sampnum < fieldstart[curfield + 1]) break; printf("Field %5d: Edge found at offset %d (frame %.1f)\n", firstfield + curfield, sampnum - fieldstart[curfield], (double)(firstfield + curfield) * 0.5); } // otherwise, compute the delta from the provided field number else { printf("Field %5d: Edge at offset %d from expected (found at %d, expected %d)\n", fieldnum, sampnum - targetsoundstart, sampnum, targetsoundstart); delta = sampnum - targetsoundstart; } return true; } //------------------------------------------------- // generate_one_frame - generate a single // resampled frame //------------------------------------------------- void chd_resample_compressor::generate_one_frame(uint8_t *dest, uint32_t datasize, uint32_t fieldnum) { // determine the first field needed to cover this range of samples uint32_t srcbegin = field_to_sample_number(m_info, fieldnum); int64_t dstbegin = (int64_t(srcbegin) << 24) + m_ioffset + m_islope * fieldnum; uint32_t dstbeginoffset; int32_t dstbeginfield; if (dstbegin >= 0) dstbeginfield = sample_number_to_field(m_info, dstbegin >> 24, dstbeginoffset); else { dstbeginfield = -1 - sample_number_to_field(m_info, -dstbegin >> 24, dstbeginoffset); dstbeginoffset = (field_to_sample_number(m_info, -dstbeginfield) - field_to_sample_number(m_info, -dstbeginfield - 1)) - dstbeginoffset; } // determine the last field needed to cover this range of samples uint32_t srcend = field_to_sample_number(m_info, fieldnum + 1); int64_t dstend = (int64_t(srcend) << 24) + m_ioffset + m_islope * (fieldnum + 1); uint32_t dstendoffset; int32_t dstendfield; if (dstend >= 0) dstendfield = sample_number_to_field(m_info, dstend >> 24, dstendoffset); else { dstendfield = -1 - -sample_number_to_field(m_info, -dstend >> 24, dstendoffset); dstendoffset = (field_to_sample_number(m_info, -dstendfield) - field_to_sample_number(m_info, -dstendfield - 1)) - dstendoffset; } /* printf("%5d: start=%10d (%5d.%03d) end=%10d (%5d.%03d)\n", fieldnum, (int32_t)(dstbegin >> 24), dstbeginfield, dstbeginoffset, (int32_t)(dstend >> 24), dstendfield, dstendoffset); */ // read all samples required into the end of the sound buffers uint32_t dstoffset = srcend - srcbegin; for (int32_t dstfield = dstbeginfield; dstfield <= dstendfield; dstfield++) { if (dstfield >= 0) read_chd(m_source, dstfield, m_info, dstoffset); else { m_info.samples = field_to_sample_number(m_info, -dstfield) - field_to_sample_number(m_info, -dstfield - 1); memset(&m_info.lsound[dstoffset], 0, m_info.samples * sizeof(m_info.lsound[0])); memset(&m_info.rsound[dstoffset], 0, m_info.samples * sizeof(m_info.rsound[0])); } dstoffset += m_info.samples; } // resample the destination samples to the source dstoffset = srcend - srcbegin; int64_t dstpos = dstbegin; int64_t dststep = (dstend - dstbegin) / int64_t(srcend - srcbegin); for (uint32_t srcoffset = 0; srcoffset < srcend - srcbegin; srcoffset++) { m_info.lsound[srcoffset] = m_info.lsound[(int)(dstoffset + dstbeginoffset + (dstpos >> 24) - (dstbegin >> 24))]; m_info.rsound[srcoffset] = m_info.rsound[(int)(dstoffset + dstbeginoffset + (dstpos >> 24) - (dstbegin >> 24))]; dstpos += dststep; } // read the original frame, pointing the sound buffer past where we've calculated read_chd(m_source, fieldnum, m_info, srcend - srcbegin); // assemble the final frame std::vector buffer; int16_t *sampledata[2] = { &m_info.lsound[0], &m_info.rsound[0] }; avhuff_encoder::assemble_data(buffer, m_info.bitmap, m_info.channels, m_info.samples, sampledata); memcpy(dest, &buffer[0], std::min(buffer.size(), size_t(datasize))); if (buffer.size() < datasize) memset(&dest[buffer.size()], 0, datasize - buffer.size()); } //------------------------------------------------- // usage - display program usage //------------------------------------------------- static int usage(void) { fprintf(stderr, "Usage: \n"); fprintf(stderr, " ldresample source.chd\n"); fprintf(stderr, " ldresample source.chd output.chd offset [slope]\n"); fprintf(stderr, "\n"); fprintf(stderr, "Where offset and slope make a linear equation f(x) which\n"); fprintf(stderr, "describes the sample offset from the source as a function\n"); fprintf(stderr, "of field number.\n"); return 1; } //------------------------------------------------- // main - main entry point //------------------------------------------------- int main(int argc, char *argv[]) { // verify arguments if (argc < 2) return usage(); const char *srcfilename = argv[1]; const char *dstfilename = (argc < 3) ? nullptr : argv[2]; double offset = (argc < 4) ? 0.0 : atof(argv[3]); double slope = (argc < 5) ? 1.0 : atof(argv[4]); // print basic information printf("Input file: %s\n", srcfilename); if (dstfilename != nullptr) { printf("Output file: %s\n", dstfilename); printf("Offset: %f\n", offset); printf("Slope: %f\n", slope); } // open the source file chd_file srcfile; movie_info info; chd_error err = open_chd(srcfile, srcfilename, info); if (err != CHDERR_NONE) { fprintf(stderr, "Unable to open file '%s'\n", srcfilename); return 1; } // output some basics printf("Video dimensions: %dx%d\n", info.width, info.height); printf("Video frame rate: %.2fHz\n", info.framerate); printf("Sample rate: %dHz\n", info.samplerate); printf("Total fields: %d\n", info.numfields); // if we don't have a destination file, scan for edges if (dstfilename == nullptr) { for (uint32_t fieldnum = 60; fieldnum < info.numfields - 60; fieldnum += 30) { fprintf(stderr, "Field %5d\r", fieldnum); int32_t delta; find_edge_near_field(srcfile, fieldnum, info, true, delta); } } // otherwise, resample the source to the destination else { // open the destination file chd_resample_compressor dstfile(srcfile, info, int64_t(offset * 65536.0 * 256.0), int64_t(slope * 65536.0 * 256.0)); err = create_chd(dstfile, dstfilename, srcfile, info); if (!dstfile.opened()) { fprintf(stderr, "Unable to create file '%s'\n", dstfilename); return 1; } // loop over all the fields in the source file double progress, ratio; osd_ticks_t last_update = 0; while (dstfile.compress_continue(progress, ratio) == CHDERR_COMPRESSING) if (osd_ticks() - last_update > osd_ticks_per_second() / 4) { last_update = osd_ticks(); printf("Processing, %.1f%% complete....\r", progress * 100.0); } } return 0; }