summaryrefslogtreecommitdiffstatshomepage
path: root/src/tools
diff options
context:
space:
mode:
author Aaron Giles <aaron@aarongiles.com>2008-10-16 08:47:06 +0000
committer Aaron Giles <aaron@aarongiles.com>2008-10-16 08:47:06 +0000
commit819c231992043b0201408f2cfba3807bffbdcf5d (patch)
tree317a6383c8fa531dbbb01697a3d71152aadfab6d /src/tools
parenta1c2887d783817fe3678227e4ab63acc0cfab080 (diff)
Added new tool ldresample to assist in resynchronizing audio tracks in
a CHD with frames. Currently still WIP but useful if you know what you're doing. [Aaron Giles]
Diffstat (limited to 'src/tools')
-rw-r--r--src/tools/ldresample.c663
-rw-r--r--src/tools/ldverify.c2
-rw-r--r--src/tools/tools.mak14
3 files changed, 678 insertions, 1 deletions
diff --git a/src/tools/ldresample.c b/src/tools/ldresample.c
new file mode 100644
index 00000000000..f5ce0d19008
--- /dev/null
+++ b/src/tools/ldresample.c
@@ -0,0 +1,663 @@
+/***************************************************************************
+
+ ldresample.c
+
+ Laserdisc audio synchronizer and resampler.
+
+ Copyright Nicola Salmoria and the MAME Team.
+ Visit http://mamedev.org for licensing and usage restrictions.
+
+***************************************************************************/
+
+#include <stdio.h>
+#include <ctype.h>
+#include <math.h>
+#include "bitmap.h"
+#include "chd.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 */
+#define MAXIMUM_WINDOW_SIZE 40
+
+/* number of standard deviations away from silence that we consider a real signal */
+#define SIGNAL_DEVIATIONS 100
+
+/* number of standard deviations away from silence that we consider the start of a signal */
+#define SIGNAL_START_DEVIATIONS 5
+
+/* number of consecutive entries of signal before we consider that we found it */
+#define MINIMUM_SIGNAL_COUNT 20
+
+
+
+/***************************************************************************
+ TYPE DEFINITIONS
+***************************************************************************/
+
+typedef struct _movie_info movie_info;
+struct _movie_info
+{
+ double framerate;
+ int iframerate;
+ int numfields;
+ int width;
+ int height;
+ int samplerate;
+ int channels;
+ int interlaced;
+ bitmap_t * bitmap;
+ INT16 * lsound;
+ INT16 * rsound;
+ UINT32 samples;
+};
+
+
+
+/***************************************************************************
+ GLOBAL VARIABLES
+***************************************************************************/
+
+
+
+/***************************************************************************
+ INLINE FUNCTIONS
+***************************************************************************/
+
+/*-------------------------------------------------
+ field_to_sample_number - given a field number
+ compute the absolute sample number for the
+ first sample of that field
+-------------------------------------------------*/
+
+INLINE UINT32 field_to_sample_number(const movie_info *info, UINT32 field)
+{
+ return ((UINT64)info->samplerate * (UINT64)field * (UINT64)1000000 + info->iframerate - 1) / (UINT64)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 sample_number_to_field(const movie_info *info, UINT32 samplenum, UINT32 *offset)
+{
+ UINT32 guess = ((UINT64)samplenum * (UINT64)info->iframerate + ((UINT64)info->samplerate * (UINT64)1000000 - 1)) / ((UINT64)info->samplerate * (UINT64)1000000);
+ while (1)
+ {
+ UINT32 fieldstart = field_to_sample_number(info, guess);
+ UINT32 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
+***************************************************************************/
+
+/*-------------------------------------------------
+ chd_allocate_buffers - allocate buffers for
+ CHD I/O
+-------------------------------------------------*/
+
+static int chd_allocate_buffers(movie_info *info)
+{
+ /* allocate a bitmap */
+ info->bitmap = bitmap_alloc(info->width, info->height, BITMAP_FORMAT_YUY16);
+ if (info->bitmap == NULL)
+ {
+ fprintf(stderr, "Out of memory creating %dx%d bitmap\n", info->width, info->height);
+ return FALSE;
+ }
+
+ /* allocate sound buffers */
+ info->lsound = malloc(info->samplerate * sizeof(*info->lsound));
+ info->rsound = malloc(info->samplerate * sizeof(*info->rsound));
+ if (info->lsound == NULL || info->rsound == NULL)
+ {
+ fprintf(stderr, "Out of memory allocating sound buffers of %d bytes\n", (INT32)(info->samplerate * sizeof(*info->rsound)));
+ return FALSE;
+ }
+ return TRUE;
+}
+
+
+/*-------------------------------------------------
+ chd_free_buffers - release buffers for
+ CHD I/O
+-------------------------------------------------*/
+
+static void chd_free_buffers(movie_info *info)
+{
+ if (info->bitmap != NULL)
+ free(info->bitmap);
+ if (info->lsound != NULL)
+ free(info->lsound);
+ if (info->rsound != NULL)
+ free(info->rsound);
+}
+
+
+/*-------------------------------------------------
+ open_chd - open a CHD file and return
+ information about it
+-------------------------------------------------*/
+
+static chd_file *open_chd(const char *filename, movie_info *info)
+{
+ int fps, fpsfrac, width, height, interlaced, channels, rate;
+ char metadata[256];
+ chd_error chderr;
+ chd_file *chd;
+
+ /* open the file */
+ chderr = chd_open(filename, CHD_OPEN_READ, NULL, &chd);
+ if (chderr != CHDERR_NONE)
+ {
+ fprintf(stderr, "Error opening CHD file: %s\n", chd_error_string(chderr));
+ return NULL;
+ }
+
+ /* get the metadata */
+ chderr = chd_get_metadata(chd, AV_METADATA_TAG, 0, metadata, sizeof(metadata), NULL, NULL);
+ if (chderr != CHDERR_NONE)
+ {
+ fprintf(stderr, "Error getting A/V metadata: %s\n", chd_error_string(chderr));
+ chd_close(chd);
+ return NULL;
+ }
+
+ /* extract the info */
+ if (sscanf(metadata, AV_METADATA_FORMAT, &fps, &fpsfrac, &width, &height, &interlaced, &channels, &rate) != 7)
+ {
+ fprintf(stderr, "Improperly formatted metadata\n");
+ chd_close(chd);
+ return NULL;
+ }
+
+ /* extract movie info */
+ info->iframerate = fps * 1000000 + fpsfrac;
+ info->framerate = info->iframerate / 1000000.0;
+ info->numfields = chd_get_header(chd)->totalhunks;
+ info->width = width;
+ info->height = height;
+ info->interlaced = interlaced;
+ info->samplerate = rate;
+ info->channels = channels;
+
+ /* allocate buffers */
+ if (!chd_allocate_buffers(info))
+ return NULL;
+
+ return chd;
+}
+
+
+/*-------------------------------------------------
+ create_chd - create a new CHD file
+-------------------------------------------------*/
+
+static chd_file *create_chd(const char *filename, chd_file *source, const movie_info *info)
+{
+ const chd_header *srcheader = chd_get_header(source);
+ chd_error chderr;
+ chd_file *chd;
+
+ /* create the file */
+ chderr = chd_create(filename, srcheader->logicalbytes, srcheader->hunkbytes, CHDCOMPRESSION_AV, NULL);
+ if (chderr != CHDERR_NONE)
+ {
+ fprintf(stderr, "Error creating new CHD file: %s\n", chd_error_string(chderr));
+ return NULL;
+ }
+
+ /* open the file */
+ chderr = chd_open(filename, CHD_OPEN_READWRITE, NULL, &chd);
+ if (chderr != CHDERR_NONE)
+ {
+ fprintf(stderr, "Error opening new CHD file: %s\n", chd_error_string(chderr));
+ return NULL;
+ }
+
+ /* clone the metadata */
+ chderr = chd_clone_metadata(source, chd);
+ if (chderr != CHDERR_NONE)
+ {
+ fprintf(stderr, "Error cloning metadata: %s\n", chd_error_string(chderr));
+ chd_close(chd);
+ return NULL;
+ }
+
+ /* begin compressing */
+ chderr = chd_compress_begin(chd);
+ if (chderr != CHDERR_NONE)
+ {
+ fprintf(stderr, "Error beginning compression: %s\n", chd_error_string(chderr));
+ return NULL;
+ }
+
+ return chd;
+}
+
+
+/*-------------------------------------------------
+ read_chd - read a field from a CHD file
+-------------------------------------------------*/
+
+static int read_chd(chd_file *file, UINT32 field, movie_info *info, UINT32 soundoffs)
+{
+ av_codec_decompress_config avconfig = { 0 };
+ chd_error chderr;
+
+ /* configure the codec */
+ avconfig.video = info->bitmap;
+ avconfig.maxsamples = 48000;
+ avconfig.actsamples = &info->samples;
+ avconfig.audio[0] = info->lsound + soundoffs;
+ avconfig.audio[1] = info->rsound + soundoffs;
+
+ /* configure the decompressor for this field */
+ chd_codec_config(file, AV_CODEC_DECOMPRESS_CONFIG, &avconfig);
+
+ /* read the field */
+ chderr = chd_read(file, field, NULL);
+ if (chderr != CHDERR_NONE)
+ return FALSE;
+
+ return TRUE;
+}
+
+
+/*-------------------------------------------------
+ write_chd - write a field to a CHD file
+-------------------------------------------------*/
+
+static int write_chd(chd_file *file, UINT32 field, movie_info *info)
+{
+ av_codec_compress_config avconfig = { 0 };
+ chd_error chderr;
+
+ /* configure the codec */
+ avconfig.video = info->bitmap;
+ avconfig.channels = 2;
+ avconfig.samples = info->samples;
+ avconfig.audio[0] = info->lsound;
+ avconfig.audio[1] = info->rsound;
+
+ /* configure the decompressor for this field */
+ chd_codec_config(file, AV_CODEC_COMPRESS_CONFIG, &avconfig);
+
+ /* read the field */
+ chderr = chd_compress_hunk(file, NULL, NULL);
+ if (chderr != CHDERR_NONE)
+ return FALSE;
+
+ return TRUE;
+}
+
+
+/*-------------------------------------------------
+ create_close_chd - close a CHD file
+-------------------------------------------------*/
+
+static void create_close_chd(chd_file *file)
+{
+ chd_error err;
+
+ err = chd_compress_finish(file);
+ if (err != CHDERR_NONE)
+ fprintf(stderr, "Error finishing compression: %s\n", chd_error_string(err));
+
+ chd_close(file);
+}
+
+
+/*-------------------------------------------------
+ close_chd - close a CHD file
+-------------------------------------------------*/
+
+static void close_chd(chd_file *file, movie_info *info)
+{
+ if (info != NULL)
+ chd_free_buffers(info);
+ chd_close(file);
+}
+
+
+
+/***************************************************************************
+ CORE IMPLEMENTATION
+***************************************************************************/
+
+/*-------------------------------------------------
+ find_edge_near_field - given a field number,
+ load +/- 1/2 second on either side and find
+ an audio edge
+-------------------------------------------------*/
+
+static int find_edge_near_field(chd_file *srcfile, UINT32 fieldnum, movie_info *info, int report_best_field, INT32 *delta)
+{
+ int fields_to_read = info->iframerate / 1000000;
+ UINT32 firstlavg = 0, firstravg = 0;
+ UINT32 firstldev = 0, firstrdev = 0;
+ UINT32 lcount = 0, rcount = 0;
+ UINT32 targetsoundstart = 0;
+ UINT32 firstfieldend = 0;
+ INT32 firstfield, curfield;
+ UINT32 fieldstart[100];
+ UINT32 soundend = 0;
+ UINT32 sampnum;
+
+ /* clear the sound buffers */
+ memset(info->lsound, 0, info->samplerate * sizeof(*info->lsound));
+ memset(info->rsound, 0, info->samplerate * sizeof(*info->rsound));
+
+ /* read 1 second around the target area */
+ firstfield = fieldnum - (fields_to_read / 2);
+ for (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 (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 (sampnum = 0; sampnum < soundend - MAXIMUM_WINDOW_SIZE; sampnum++)
+ {
+ UINT32 lmax = 0, rmax = 0;
+ UINT32 scannum;
+
+ /* scan forward over the maximum window */
+ for (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 */
+ for (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 */
+ for (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 */
+ 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)
+ {
+ 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;
+}
+
+
+/*-------------------------------------------------
+ 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[])
+{
+ movie_info info = { 0 };
+ const char *srcfilename;
+ const char *dstfilename;
+ double offset, slope;
+ chd_file *srcfile;
+ chd_file *dstfile;
+
+ /* verify arguments */
+ if (argc < 2)
+ return usage();
+ srcfilename = argv[1];
+ dstfilename = (argc < 3) ? NULL : argv[2];
+ offset = (argc < 4) ? 0.0 : atof(argv[3]);
+ slope = (argc < 5) ? 1.0 : atof(argv[4]);
+
+ /* print basic information */
+ printf("Input file: %s\n", srcfilename);
+ if (dstfilename != NULL)
+ {
+ printf("Output file: %s\n", dstfilename);
+ printf("Offset: %f\n", offset);
+ printf("Slope: %f\n", slope);
+ }
+
+ /* open the source file */
+ srcfile = open_chd(srcfilename, &info);
+ if (srcfile == NULL)
+ {
+ 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 == NULL)
+ {
+ UINT32 fieldnum;
+ INT32 delta;
+
+ for (fieldnum = 60; fieldnum < info.numfields - 60; fieldnum += 30)
+ {
+ fprintf(stderr, "Field %5d\r", fieldnum);
+ find_edge_near_field(srcfile, fieldnum, &info, TRUE, &delta);
+ }
+ }
+
+ /* otherwise, resample the source to the destination */
+ else
+ {
+ INT64 ioffset = (INT64)(offset * 65536.0 * 256.0);
+ INT64 islope = (INT64)(slope * 65536.0 * 256.0);
+ UINT32 fieldnum;
+
+ /* open the destination file */
+ dstfile = create_chd(dstfilename, srcfile, &info);
+ if (dstfile == NULL)
+ {
+ fprintf(stderr, "Unable to create file '%s'\n", dstfilename);
+ return 1;
+ }
+
+ /* loop over all the fields in the source file */
+ for (fieldnum = 0; fieldnum < info.numfields; fieldnum++)
+ {
+ UINT32 srcbegin = field_to_sample_number(&info, fieldnum);
+ UINT32 srcend = field_to_sample_number(&info, fieldnum + 1);
+ INT64 dstbegin = ((INT64)srcbegin << 24) + ioffset + islope * fieldnum;
+ INT64 dstend = ((INT64)srcend << 24) + ioffset + islope * (fieldnum + 1);
+ UINT32 dstbeginoffset, dstendoffset, dstoffset;
+ INT32 dstbeginfield, dstendfield, dstfield;
+ INT64 dstpos, dststep;
+ UINT32 srcoffset;
+
+ /* update progress (this ain't fast!) */
+ if (fieldnum % 10 == 0)
+ fprintf(stderr, "Field %d\r", fieldnum);
+
+ /* determine the first and last fields needed to cover this range of samples */
+ if (dstbegin >= 0)
+ dstbeginfield = sample_number_to_field(&info, dstbegin >> 24, &dstbeginoffset);
+ else
+ {
+ dstbeginfield = -1 - sample_number_to_field(&info, -dstbegin >> 24, &dstbeginoffset);
+ dstbeginoffset = (field_to_sample_number(&info, -dstbeginfield) - field_to_sample_number(&info, -dstbeginfield - 1)) - dstbeginoffset;
+ }
+ if (dstend >= 0)
+ dstendfield = sample_number_to_field(&info, dstend >> 24, &dstendoffset);
+ else
+ {
+ dstendfield = -1 - -sample_number_to_field(&info, -dstend >> 24, &dstendoffset);
+ dstendoffset = (field_to_sample_number(&info, -dstendfield) - field_to_sample_number(&info, -dstendfield - 1)) - dstendoffset;
+ }
+/*
+printf("%5d: start=%10d (%5d.%03d) end=%10d (%5d.%03d)\n",
+ fieldnum,
+ (INT32)(dstbegin >> 24), dstbeginfield, dstbeginoffset,
+ (INT32)(dstend >> 24), dstendfield, dstendoffset);
+*/
+ /* read all samples required into the end of the sound buffers */
+ dstoffset = srcend - srcbegin;
+ for (dstfield = dstbeginfield; dstfield <= dstendfield; dstfield++)
+ {
+ if (dstfield >= 0)
+ read_chd(srcfile, dstfield, &info, dstoffset);
+ else
+ {
+ info.samples = field_to_sample_number(&info, -dstfield) - field_to_sample_number(&info, -dstfield - 1);
+ memset(&info.lsound[dstoffset], 0, info.samples * sizeof(info.lsound[0]));
+ memset(&info.rsound[dstoffset], 0, info.samples * sizeof(info.rsound[0]));
+ }
+ dstoffset += info.samples;
+ }
+
+ /* resample the destination samples to the source */
+ dstoffset = srcend - srcbegin;
+ dstpos = dstbegin;
+ dststep = (dstend - dstbegin) / (INT64)(srcend - srcbegin);
+ for (srcoffset = 0; srcoffset < srcend - srcbegin; srcoffset++)
+ {
+ info.lsound[srcoffset] = info.lsound[dstoffset + dstbeginoffset + (dstpos >> 24) - (dstbegin >> 24)];
+ info.rsound[srcoffset] = info.rsound[dstoffset + dstbeginoffset + (dstpos >> 24) - (dstbegin >> 24)];
+ dstpos += dststep;
+ }
+
+ /* read the original frame, pointing the sound buffer past where we've calculated */
+ read_chd(srcfile, fieldnum, &info, srcend - srcbegin);
+
+ /* write it to the destination */
+ write_chd(dstfile, fieldnum, &info);
+ }
+
+ /* close the destination file */
+ create_close_chd(dstfile);
+ }
+
+ /* close the source file */
+ close_chd(srcfile, &info);
+
+ return 0;
+}
diff --git a/src/tools/ldverify.c b/src/tools/ldverify.c
index 296a0870b7a..96ce516f855 100644
--- a/src/tools/ldverify.c
+++ b/src/tools/ldverify.c
@@ -650,7 +650,7 @@ static void verify_audio_final(void)
static int usage(void)
{
fprintf(stderr, "Usage: \n");
- fprintf(stderr, " ldverify [avifile.avi|chdfile.chd]\n");
+ fprintf(stderr, " ldverify <avifile.avi|chdfile.chd>\n");
return 1;
}
diff --git a/src/tools/tools.mak b/src/tools/tools.mak
index c9f73eac95a..d9aca2e4673 100644
--- a/src/tools/tools.mak
+++ b/src/tools/tools.mak
@@ -26,6 +26,7 @@ TOOLS += \
romcmp$(EXE) \
chdman$(EXE) \
jedutil$(EXE) \
+ ldresample$(EXE) \
ldverify$(EXE) \
regrep$(EXE) \
srcclean$(EXE) \
@@ -74,6 +75,19 @@ jedutil$(EXE): $(JEDUTILOBJS) $(LIBUTIL) $(LIBOCORE) $(ZLIB) $(EXPAT)
#-------------------------------------------------
+# ldresample
+#-------------------------------------------------
+
+LDRESAMPLEOBJS = \
+ $(TOOLSOBJ)/ldresample.o \
+
+ldresample$(EXE): $(LDRESAMPLEOBJS) $(LIBUTIL) $(LIBOCORE) $(ZLIB) $(EXPAT)
+ @echo Linking $@...
+ $(LD) $(LDFLAGS) $^ $(LIBS) -o $@
+
+
+
+#-------------------------------------------------
# ldverify
#-------------------------------------------------