#!/usr/bin/perl -w

use warnings;
use strict;

my @audiotypes = qw(
    U8
    S8
    U16LSB
    S16LSB
    U16MSB
    S16MSB
    S32LSB
    S32MSB
    F32LSB
    F32MSB
);

my @channels = ( 1, 2, 4, 6, 8 );
my %funcs;
my $custom_converters = 0;


sub getTypeConvertHashId {
    my ($from, $to) = @_;
    return "TYPECONVERTER $from/$to";
}


sub getResamplerHashId {
    my ($from, $channels, $upsample, $multiple) = @_;
    return "RESAMPLER $from/$channels/$upsample/$multiple";
}


sub outputHeader {
    print <<EOF;
/* DO NOT EDIT!  This file is generated by sdlgenaudiocvt.pl */
/*
    SDL - Simple DirectMedia Layer
    Copyright (C) 1997-2011 Sam Lantinga

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

    Sam Lantinga
    slouken\@libsdl.org
*/

#include "SDL_config.h"
#include "SDL_audio.h"
#include "SDL_audio_c.h"

#ifndef DEBUG_CONVERT
#define DEBUG_CONVERT 0
#endif


/* If you can guarantee your data and need space, you can eliminate code... */

/* Just build the arbitrary resamplers if you're saving code space. */
#ifndef LESS_RESAMPLERS
#define LESS_RESAMPLERS 0
#endif

/* Don't build any resamplers if you're REALLY saving code space. */
#ifndef NO_RESAMPLERS
#define NO_RESAMPLERS 0
#endif

/* Don't build any type converters if you're saving code space. */
#ifndef NO_CONVERTERS
#define NO_CONVERTERS 0
#endif


/* *INDENT-OFF* */

EOF

    my @vals = ( 127, 32767, 2147483647 );
    foreach (@vals) {
        my $val = $_;
        my $fval = 1.0 / $val;
        print("#define DIVBY${val} ${fval}f\n");
    }

    print("\n");
}

sub outputFooter {
    print <<EOF;
/* $custom_converters converters generated. */

/* *INDENT-ON* */

/* vi: set ts=4 sw=4 expandtab: */
EOF
}

sub splittype {
    my $t = shift;
    my ($signed, $size, $endian) = $t =~ /([USF])(\d+)([LM]SB|)/;
    my $float = ($signed eq 'F') ? 1 : 0;
    $signed = (($float) or ($signed eq 'S')) ? 1 : 0;
    $endian = 'NONE' if ($endian eq '');

    my $ctype = '';
    if ($float) {
        $ctype = (($size == 32) ? 'float' : 'double');
    } else {
        $ctype = (($signed) ? 'S' : 'U') . "int${size}";
    }

    return ($signed, $float, $size, $endian, $ctype);
}

sub getSwapFunc {
    my ($size, $signed, $float, $endian, $val) = @_;
    my $BEorLE = (($endian eq 'MSB') ? 'BE' : 'LE');
    my $code = '';

    if ($float) {
        $code = "SDL_SwapFloat${BEorLE}($val)";
    } else {
        if ($size > 8) {
            $code = "SDL_Swap${BEorLE}${size}($val)";
        } else {
            $code = $val;
        }

        if (($signed) and (!$float)) {
            $code = "((Sint${size}) $code)";
        }
    }

    return "${code}";
}


sub maxIntVal {
    my $size = shift;
    if ($size == 8) {
        return 0x7F;
    } elsif ($size == 16) {
        return 0x7FFF;
    } elsif ($size == 32) {
        return 0x7FFFFFFF;
    }

    die("bug in script.\n");
}

sub getFloatToIntMult {
    my $size = shift;
    my $val = maxIntVal($size) . '.0';
    $val .= 'f' if ($size < 32);
    return $val;
}

sub getIntToFloatDivBy {
    my $size = shift;
    return 'DIVBY' . maxIntVal($size);
}

sub getSignFlipVal {
    my $size = shift;
    if ($size == 8) {
        return '0x80';
    } elsif ($size == 16) {
        return '0x8000';
    } elsif ($size == 32) {
        return '0x80000000';
    }

    die("bug in script.\n");
}

sub buildCvtFunc {
    my ($from, $to) = @_;
    my ($fsigned, $ffloat, $fsize, $fendian, $fctype) = splittype($from);
    my ($tsigned, $tfloat, $tsize, $tendian, $tctype) = splittype($to);
    my $diffs = 0;
    $diffs++ if ($fsize != $tsize);
    $diffs++ if ($fsigned != $tsigned);
    $diffs++ if ($ffloat != $tfloat);
    $diffs++ if ($fendian ne $tendian);

    return if ($diffs == 0);

    my $hashid = getTypeConvertHashId($from, $to);
    if (1) { # !!! FIXME: if ($diffs > 1) {
        my $sym = "SDL_Convert_${from}_to_${to}";
        $funcs{$hashid} = $sym;
        $custom_converters++;

        # Always unsigned for ints, for possible byteswaps.
        my $srctype = (($ffloat) ? 'float' : "Uint${fsize}");

        print <<EOF;
static void SDLCALL
${sym}(SDL_AudioCVT * cvt, SDL_AudioFormat format)
{
    int i;
    const $srctype *src;
    $tctype *dst;

#if DEBUG_CONVERT
    fprintf(stderr, "Converting AUDIO_${from} to AUDIO_${to}.\\n");
#endif

EOF

        if ($fsize < $tsize) {
            my $mult = $tsize / $fsize;
            print <<EOF;
    src = ((const $srctype *) (cvt->buf + cvt->len_cvt)) - 1;
    dst = (($tctype *) (cvt->buf + cvt->len_cvt * $mult)) - 1;
    for (i = cvt->len_cvt / sizeof ($srctype); i; --i, --src, --dst) {
EOF
        } else {
            print <<EOF;
    src = (const $srctype *) cvt->buf;
    dst = ($tctype *) cvt->buf;
    for (i = cvt->len_cvt / sizeof ($srctype); i; --i, ++src, ++dst) {
EOF
        }

        # Have to convert to/from float/int.
        # !!! FIXME: cast through double for int32<->float?
        my $code = getSwapFunc($fsize, $fsigned, $ffloat, $fendian, '*src');
        if ($ffloat != $tfloat) {
            if ($ffloat) {
                my $mult = getFloatToIntMult($tsize);
                if (!$tsigned) {   # bump from -1.0f/1.0f to 0.0f/2.0f
                    $code = "($code + 1.0f)";
                }
                $code = "(($tctype) ($code * $mult))";
            } else {
                # $divby will be the reciprocal, to avoid pipeline stalls
                #  from floating point division...so multiply it.
                my $divby = getIntToFloatDivBy($fsize);
                $code = "(((float) $code) * $divby)";
                if (!$fsigned) {   # bump from 0.0f/2.0f to -1.0f/1.0f.
                    $code = "($code - 1.0f)";
                }
            }
        } else {
            # All integer conversions here.
            if ($fsigned != $tsigned) {
                my $signflipval = getSignFlipVal($fsize);
                $code = "(($code) ^ $signflipval)";
            }

            my $shiftval = abs($fsize - $tsize);
            if ($fsize < $tsize) {
                $code = "((($tctype) $code) << $shiftval)";
            } elsif ($fsize > $tsize) {
                $code = "(($tctype) ($code >> $shiftval))";
            }
        }

        my $swap = getSwapFunc($tsize, $tsigned, $tfloat, $tendian, 'val');

        print <<EOF;
        const $tctype val = $code;
        *dst = ${swap};
    }

EOF

        if ($fsize > $tsize) {
            my $divby = $fsize / $tsize;
            print("    cvt->len_cvt /= $divby;\n");
        } elsif ($fsize < $tsize) {
            my $mult = $tsize / $fsize;
            print("    cvt->len_cvt *= $mult;\n");
        }

        print <<EOF;
    if (cvt->filters[++cvt->filter_index]) {
        cvt->filters[cvt->filter_index] (cvt, AUDIO_$to);
    }
}

EOF

    } else {
        if ($fsigned != $tsigned) {
            $funcs{$hashid} = 'SDL_ConvertSigned';
        } elsif ($ffloat != $tfloat) {
            $funcs{$hashid} = 'SDL_ConvertFloat';
        } elsif ($fsize != $tsize) {
            $funcs{$hashid} = 'SDL_ConvertSize';
        } elsif ($fendian ne $tendian) {
            $funcs{$hashid} = 'SDL_ConvertEndian';
        } else {
            die("error in script.\n");
        }
    }
}


sub buildTypeConverters {
    print "#if !NO_CONVERTERS\n\n";
    foreach (@audiotypes) {
        my $from = $_;
        foreach (@audiotypes) {
            my $to = $_;
            buildCvtFunc($from, $to);
        }
    }
    print "#endif  /* !NO_CONVERTERS */\n\n\n";

    print "const SDL_AudioTypeFilters sdl_audio_type_filters[] =\n{\n";
    print "#if !NO_CONVERTERS\n";
    foreach (@audiotypes) {
        my $from = $_;
        foreach (@audiotypes) {
            my $to = $_;
            if ($from ne $to) {
                my $hashid = getTypeConvertHashId($from, $to);
                my $sym = $funcs{$hashid};
                print("    { AUDIO_$from, AUDIO_$to, $sym },\n");
            }
        }
    }
    print "#endif  /* !NO_CONVERTERS */\n";

    print("    { 0, 0, NULL }\n");
    print "};\n\n\n";
}

sub getBiggerCtype {
    my ($isfloat, $size) = @_;

    if ($isfloat) {
        if ($size == 32) {
            return 'double';
        }
        die("bug in script.\n");
    }

    if ($size == 8) {
        return 'Sint16';
    } elsif ($size == 16) {
        return 'Sint32'
    } elsif ($size == 32) {
        return 'Sint64'
    }

    die("bug in script.\n");
}


# These handle arbitrary resamples...44100Hz to 48000Hz, for example.
# Man, this code is skanky.
sub buildArbitraryResampleFunc {
    # !!! FIXME: we do a lot of unnecessary and ugly casting in here, due to getSwapFunc().
    my ($from, $channels, $upsample) = @_;
    my ($fsigned, $ffloat, $fsize, $fendian, $fctype) = splittype($from);

    my $bigger = getBiggerCtype($ffloat, $fsize);
    my $interp = ($ffloat) ? '* 0.5' : '>> 1';

    my $resample = ($upsample) ? 'Upsample' : 'Downsample';
    my $hashid = getResamplerHashId($from, $channels, $upsample, 0);
    my $sym = "SDL_${resample}_${from}_${channels}c";
    $funcs{$hashid} = $sym;
    $custom_converters++;

    my $fudge = $fsize * $channels * 2;  # !!! FIXME
    my $eps_adjust = ($upsample) ? 'dstsize' : 'srcsize';
    my $incr = '';
    my $incr2 = '';


    # !!! FIXME: DEBUG_CONVERT should report frequencies.
    print <<EOF;
static void SDLCALL
${sym}(SDL_AudioCVT * cvt, SDL_AudioFormat format)
{
#if DEBUG_CONVERT
    fprintf(stderr, "$resample arbitrary (x%f) AUDIO_${from}, ${channels} channels.\\n", cvt->rate_incr);
#endif

    const int srcsize = cvt->len_cvt - $fudge;
    const int dstsize = (int) (((double)cvt->len_cvt) * cvt->rate_incr);
    register int eps = 0;
EOF

    my $endcomparison = '!=';

    # Upsampling (growing the buffer) needs to work backwards, since we
    #  overwrite the buffer as we go.
    if ($upsample) {
        $endcomparison = '>';  # dst > target
        print <<EOF;
    $fctype *dst = (($fctype *) (cvt->buf + dstsize)) - $channels;
    const $fctype *src = (($fctype *) (cvt->buf + cvt->len_cvt)) - $channels;
    const $fctype *target = ((const $fctype *) cvt->buf) - $channels;
EOF
    } else {
        $endcomparison = '<';  # dst < target
        print <<EOF;
    $fctype *dst = ($fctype *) cvt->buf;
    const $fctype *src = ($fctype *) cvt->buf;
    const $fctype *target = (const $fctype *) (cvt->buf + dstsize);
EOF
    }

    for (my $i = 0; $i < $channels; $i++) {
        my $idx = ($upsample) ? (($channels - $i) - 1) : $i;
        my $val = getSwapFunc($fsize, $fsigned, $ffloat, $fendian, "src[$idx]");
        print <<EOF;
    $fctype sample${idx} = $val;
EOF
    }

    for (my $i = 0; $i < $channels; $i++) {
        my $idx = ($upsample) ? (($channels - $i) - 1) : $i;
        print <<EOF;
    $fctype last_sample${idx} = sample${idx};
EOF
    }

    print <<EOF;
    while (dst $endcomparison target) {
EOF

    if ($upsample) {
        for (my $i = 0; $i < $channels; $i++) {
            # !!! FIXME: don't do this swap every write, just when the samples change.
            my $idx = (($channels - $i) - 1);
            my $val = getSwapFunc($fsize, $fsigned, $ffloat, $fendian, "sample${idx}");
            print <<EOF;
        dst[$idx] = $val;
EOF
        }

        $incr = ($channels == 1) ? 'dst--' : "dst -= $channels";
        $incr2 = ($channels == 1) ? 'src--' : "src -= $channels";

        print <<EOF;
        $incr;
        eps += srcsize;
        if ((eps << 1) >= dstsize) {
            $incr2;
EOF
    } else {  # downsample.
        $incr = ($channels == 1) ? 'src++' : "src += $channels";
        print <<EOF;
        $incr;
        eps += dstsize;
        if ((eps << 1) >= srcsize) {
EOF
        for (my $i = 0; $i < $channels; $i++) {
            my $val = getSwapFunc($fsize, $fsigned, $ffloat, $fendian, "sample${i}");
            print <<EOF;
            dst[$i] = $val;
EOF
        }

        $incr = ($channels == 1) ? 'dst++' : "dst += $channels";
        print <<EOF;
            $incr;
EOF
    }

    for (my $i = 0; $i < $channels; $i++) {
        my $idx = ($upsample) ? (($channels - $i) - 1) : $i;
        my $swapped = getSwapFunc($fsize, $fsigned, $ffloat, $fendian, "src[$idx]");
        print <<EOF;
            sample${idx} = ($fctype) (((($bigger) $swapped) + (($bigger) last_sample${idx})) $interp);
EOF
    }

    for (my $i = 0; $i < $channels; $i++) {
        my $idx = ($upsample) ? (($channels - $i) - 1) : $i;
        print <<EOF;
            last_sample${idx} = sample${idx};
EOF
    }

    print <<EOF;
            eps -= $eps_adjust;
        }
    }
EOF

        print <<EOF;
    cvt->len_cvt = dstsize;
    if (cvt->filters[++cvt->filter_index]) {
        cvt->filters[cvt->filter_index] (cvt, format);
    }
}

EOF

}

# These handle clean resamples...doubling and quadrupling the sample rate, etc.
sub buildMultipleResampleFunc {
    # !!! FIXME: we do a lot of unnecessary and ugly casting in here, due to getSwapFunc().
    my ($from, $channels, $upsample, $multiple) = @_;
    my ($fsigned, $ffloat, $fsize, $fendian, $fctype) = splittype($from);

    my $bigger = getBiggerCtype($ffloat, $fsize);
    my $interp = ($ffloat) ? '* 0.5' : '>> 1';
    my $interp2 = ($ffloat) ? '* 0.25' : '>> 2';
    my $mult3 = ($ffloat) ? '3.0' : '3';
    my $lencvtop = ($upsample) ? '*' : '/';

    my $resample = ($upsample) ? 'Upsample' : 'Downsample';
    my $hashid = getResamplerHashId($from, $channels, $upsample, $multiple);
    my $sym = "SDL_${resample}_${from}_${channels}c_x${multiple}";
    $funcs{$hashid} = $sym;
    $custom_converters++;

    # !!! FIXME: DEBUG_CONVERT should report frequencies.
    print <<EOF;
static void SDLCALL
${sym}(SDL_AudioCVT * cvt, SDL_AudioFormat format)
{
#if DEBUG_CONVERT
    fprintf(stderr, "$resample (x${multiple}) AUDIO_${from}, ${channels} channels.\\n");
#endif

    const int srcsize = cvt->len_cvt;
    const int dstsize = cvt->len_cvt $lencvtop $multiple;
EOF

    my $endcomparison = '!=';

    # Upsampling (growing the buffer) needs to work backwards, since we
    #  overwrite the buffer as we go.
    if ($upsample) {
        $endcomparison = '>';  # dst > target
        print <<EOF;
    $fctype *dst = (($fctype *) (cvt->buf + dstsize)) - $channels;
    const $fctype *src = (($fctype *) (cvt->buf + cvt->len_cvt)) - $channels;
    const $fctype *target = ((const $fctype *) cvt->buf) - $channels;
EOF
    } else {
        $endcomparison = '<';  # dst < target
        print <<EOF;
    $fctype *dst = ($fctype *) cvt->buf;
    const $fctype *src = ($fctype *) cvt->buf;
    const $fctype *target = (const $fctype *) (cvt->buf + dstsize);
EOF
    }

    for (my $i = 0; $i < $channels; $i++) {
        my $idx = ($upsample) ? (($channels - $i) - 1) : $i;
        my $val = getSwapFunc($fsize, $fsigned, $ffloat, $fendian, "src[$idx]");
        print <<EOF;
    $bigger last_sample${idx} = ($bigger) $val;
EOF
    }

    print <<EOF;
    while (dst $endcomparison target) {
EOF

    for (my $i = 0; $i < $channels; $i++) {
        my $idx = ($upsample) ? (($channels - $i) - 1) : $i;
        my $val = getSwapFunc($fsize, $fsigned, $ffloat, $fendian, "src[$idx]");
        print <<EOF;
        const $bigger sample${idx} = ($bigger) $val;
EOF
    }

    my $incr = '';
    if ($upsample) {
        $incr = ($channels == 1) ? 'src--' : "src -= $channels";
    } else {
        my $amount = $channels * $multiple;
        $incr = "src += $amount";  # can't ever be 1, so no "++" version.
    }


    print <<EOF;
        $incr;
EOF

    # !!! FIXME: This really begs for some Altivec or SSE, etc.
    if ($upsample) {
        if ($multiple == 2) {
            for (my $i = $channels-1; $i >= 0; $i--) {
                my $dsti = $i + $channels;
                print <<EOF;
        dst[$dsti] = ($fctype) ((sample${i} + last_sample${i}) $interp);
EOF
            }
            for (my $i = $channels-1; $i >= 0; $i--) {
                my $dsti = $i;
                print <<EOF;
        dst[$dsti] = ($fctype) sample${i};
EOF
            }
        } elsif ($multiple == 4) {
            for (my $i = $channels-1; $i >= 0; $i--) {
                my $dsti = $i + ($channels * 3);
                print <<EOF;
        dst[$dsti] = ($fctype) sample${i};
EOF
            }

            for (my $i = $channels-1; $i >= 0; $i--) {
                my $dsti = $i + ($channels * 2);
                print <<EOF;
        dst[$dsti] = ($fctype) ((($mult3 * sample${i}) + last_sample${i}) $interp2);
EOF
            }

            for (my $i = $channels-1; $i >= 0; $i--) {
                my $dsti = $i + ($channels * 1);
                print <<EOF;
        dst[$dsti] = ($fctype) ((sample${i} + last_sample${i}) $interp);
EOF
            }

            for (my $i = $channels-1; $i >= 0; $i--) {
                my $dsti = $i + ($channels * 0);
                print <<EOF;
        dst[$dsti] = ($fctype) ((sample${i} + ($mult3 * last_sample${i})) $interp2);
EOF
            }
        } else {
            die('bug in program.');  # we only handle x2 and x4.
        }
    } else {  # downsample.
        if ($multiple == 2) {
            for (my $i = 0; $i < $channels; $i++) {
                print <<EOF;
        dst[$i] = ($fctype) ((sample${i} + last_sample${i}) $interp);
EOF
            }
        } elsif ($multiple == 4) {
            # !!! FIXME: interpolate all 4 samples?
            for (my $i = 0; $i < $channels; $i++) {
                print <<EOF;
        dst[$i] = ($fctype) ((sample${i} + last_sample${i}) $interp);
EOF
            }
        } else {
            die('bug in program.');  # we only handle x2 and x4.
        }
    }

    for (my $i = 0; $i < $channels; $i++) {
        my $idx = ($upsample) ? (($channels - $i) - 1) : $i;
        print <<EOF;
        last_sample${idx} = sample${idx};
EOF
    }

    if ($upsample) {
        my $amount = $channels * $multiple;
        $incr = "dst -= $amount";  # can't ever be 1, so no "--" version.
    } else {
        $incr = ($channels == 1) ? 'dst++' : "dst += $channels";
    }

    print <<EOF;
        $incr;
    }

    cvt->len_cvt = dstsize;
    if (cvt->filters[++cvt->filter_index]) {
        cvt->filters[cvt->filter_index] (cvt, format);
    }
}

EOF

}

sub buildResamplers {
    print "#if !NO_RESAMPLERS\n\n";
    foreach (@audiotypes) {
        my $from = $_;
        foreach (@channels) {
            my $channel = $_;
            buildArbitraryResampleFunc($from, $channel, 1);
            buildArbitraryResampleFunc($from, $channel, 0);
        }
    }

    print "\n#if !LESS_RESAMPLERS\n\n";
    foreach (@audiotypes) {
        my $from = $_;
        foreach (@channels) {
            my $channel = $_;
            for (my $multiple = 2; $multiple <= 4; $multiple += 2) {
                buildMultipleResampleFunc($from, $channel, 1, $multiple);
                buildMultipleResampleFunc($from, $channel, 0, $multiple);
            }
        }
    }

    print "#endif  /* !LESS_RESAMPLERS */\n";
    print "#endif  /* !NO_RESAMPLERS */\n\n\n";

    print "const SDL_AudioRateFilters sdl_audio_rate_filters[] =\n{\n";
    print "#if !NO_RESAMPLERS\n";
    foreach (@audiotypes) {
        my $from = $_;
        foreach (@channels) {
            my $channel = $_;
            for (my $upsample = 0; $upsample <= 1; $upsample++) {
                my $hashid = getResamplerHashId($from, $channel, $upsample, 0);
                my $sym = $funcs{$hashid};
                print("    { AUDIO_$from, $channel, $upsample, 0, $sym },\n");
            }
        }
    }

    print "#if !LESS_RESAMPLERS\n";
    foreach (@audiotypes) {
        my $from = $_;
        foreach (@channels) {
            my $channel = $_;
            for (my $multiple = 2; $multiple <= 4; $multiple += 2) {
                for (my $upsample = 0; $upsample <= 1; $upsample++) {
                    my $hashid = getResamplerHashId($from, $channel, $upsample, $multiple);
                    my $sym = $funcs{$hashid};
                    print("    { AUDIO_$from, $channel, $upsample, $multiple, $sym },\n");
                }
            }
        }
    }

    print "#endif  /* !LESS_RESAMPLERS */\n";
    print "#endif  /* !NO_RESAMPLERS */\n";
    print("    { 0, 0, 0, 0, NULL }\n");
    print "};\n\n";
}


# mainline ...

outputHeader();
buildTypeConverters();
buildResamplers();
outputFooter();

exit 0;

# end of sdlgenaudiocvt.pl ...

