wine/tools/make_fir
2012-05-08 09:29:47 +02:00

159 lines
5.0 KiB
Perl
Executable File

#! /usr/bin/perl -w
#
# DirectSound
#
# Copyright 2011-2012 Alexander E. Patrakov
#
# 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
use strict;
use Math::Trig;
# This program generates an array of Finite Impulse Response (FIR) filter
# values for use in resampling audio.
#
# Values are based on the resampler from Windows XP at the default (best)
# quality, reverse engineered by saving kvm output to a wav file.
# Controls how sharp the transition between passband and stopband is.
# The transition bandwidth is approximately (1 / exp_width) of the
# Nyquist frequency.
my $exp_width = 41.0;
# Controls the stopband attenuation. It is related but not proportional
# to exp(-(PI * lobes_per_wing / exp_width) ^2) / lobes_per_wing
my $lobes_per_wing = 28;
# Controls the position of the transition band and thus attenuation at the
# Nyquist frequency and above. Amended below so that the length of the FIR is
# an integer. Essentially, this controls the trade-off between good rejection
# of unrepresentable frequencies (those above half of the lower of the sample
# rates) and not rejecting the wanted ones. Windows XP errs on the side of
# letting artifacts through, which somewhat makes sense if they are above
# 20 kHz anyway, or in the case of upsampling, where we can assume that the
# problematic frequencies are not in the input. This, however, doesn't match
# what linux resamplers do - so set this to 0.85 to match them. 0.98 would
# give Windows XP behaviour.
my $approx_bandwidth = 0.85;
# The amended value will be stored here
my $bandwidth;
# The number of points per time unit equal to one period of the original
# Nyquist frequency. The more points, the less interpolation error is.
my $fir_step = 120;
# Here x is measured in half-periods of the lower sample rate
sub fir_val($)
{
my ($x) = @_;
$x *= pi * $bandwidth;
my $s = $x / $exp_width;
my $sinc = $x ? (sin($x) / $x) : 1.0;
my $gauss = exp(-($s * $s));
return $sinc * $gauss;
}
# Linear interpolation
sub mlinear($$$)
{
my ($y1, $y2, $mu) = @_;
return $y1 * (1.0 - $mu) + $y2 * $mu;
}
# to_db, for printing decibel values
sub to_db($) {
my ($x) = @_;
return 20.0 * log(abs($x))/log(10.0);
}
my $wing_len = int($lobes_per_wing / $approx_bandwidth * $fir_step + 1);
$bandwidth = 1.0 * $lobes_per_wing / $wing_len;
my $amended_bandwidth = $bandwidth * $fir_step;
my $fir_len = 2 * $wing_len + 1;
my @fir;
# Constructing the FIR is easy
for (my $i = 0; $i < $fir_len; $i++) {
push @fir, fir_val($i - $wing_len);
}
# Now we have to test it and print some statistics to stderr.
# Test 0: FIR size
print STDERR "size: $fir_len\n";
# Test 1: Interpolation noise. It should be less than -90 dB.
# If you suspect that 0.5 is special due to some symmetry and thus yields
# an abnormally low noise figure, change it. But really, it isn't special.
my $testpoint = 0.5;
my $exact_val = fir_val($testpoint);
my $lin_approx_val = mlinear($fir[$wing_len], $fir[$wing_len + 1],
$testpoint);
my $lin_error_db = to_db($exact_val - $lin_approx_val);
printf STDERR "interpolation noise: %1.2f dB\n", $lin_error_db;
# Test 2: Passband and stopband.
# The filter gain, ideally, should be 0.00 dB below the Nyquist
# frequency and -inf dB above it. But it is impossible. So
# let's settle for -80 dB above 1.08 * f_Nyquist.
my $sum = 0.0;
$sum += $_ for @fir;
# Frequencies in this list are expressed as fractions
# of the Nyquist frequency.
my @testfreqs = (0.5, 0.8, 1.0, 1.08, 1.18, 1.33, 1.38);
foreach my $testfreq(@testfreqs) {
my $dct_coeff = 0.0;
for (my $i = 0; $i < $fir_len; $i++) {
my $x = 1.0 * ($i - $wing_len) / $fir_step;
$dct_coeff += $fir[$i] * cos($x * $testfreq * pi);
}
printf STDERR "DCT: %1.2f -> %1.2f dB\n",
$testfreq, to_db($dct_coeff / $sum);
}
# Now actually print the FIR to a C header file
chdir ".." if -f "./make_fir";
open FILE, ">dlls/dsound/fir.h";
select FILE;
print "/* generated by tools/make_fir; DO NOT EDIT! */\n";
print "static const int fir_len = $fir_len;\n";
print "static const int fir_step = $fir_step;\n";
print "static const float fir[] = {\n";
for (my $i = 0; $i < $fir_len; $i++) {
printf "%10.10f", $amended_bandwidth * $fir[$i];
if ($i == $fir_len - 1) {
print "\n";
} elsif (($i + 1) % 5 == 0) {
print ",\n";
} else {
print ", ";
}
}
print "};\n";