aboutsummaryrefslogtreecommitdiff
path: root/usr.sbin/virtual_oss/virtual_equalizer
diff options
context:
space:
mode:
Diffstat (limited to 'usr.sbin/virtual_oss/virtual_equalizer')
-rw-r--r--usr.sbin/virtual_oss/virtual_equalizer/Makefile11
-rw-r--r--usr.sbin/virtual_oss/virtual_equalizer/equalizer.c431
-rw-r--r--usr.sbin/virtual_oss/virtual_equalizer/virtual_equalizer.8127
3 files changed, 569 insertions, 0 deletions
diff --git a/usr.sbin/virtual_oss/virtual_equalizer/Makefile b/usr.sbin/virtual_oss/virtual_equalizer/Makefile
new file mode 100644
index 000000000000..e67150eff000
--- /dev/null
+++ b/usr.sbin/virtual_oss/virtual_equalizer/Makefile
@@ -0,0 +1,11 @@
+PROG= virtual_equalizer
+MAN= ${PROG}.8
+
+SRCS= equalizer.c
+
+CFLAGS+= -I${SRCTOP}/usr.sbin/virtual_oss/virtual_oss \
+ -I/usr/local/include
+
+LDFLAGS+= -L/usr/local/lib -lm -lfftw3
+
+.include <bsd.prog.mk>
diff --git a/usr.sbin/virtual_oss/virtual_equalizer/equalizer.c b/usr.sbin/virtual_oss/virtual_equalizer/equalizer.c
new file mode 100644
index 000000000000..d1682186084d
--- /dev/null
+++ b/usr.sbin/virtual_oss/virtual_equalizer/equalizer.c
@@ -0,0 +1,431 @@
+/*-
+ * Copyright (c) 2019 Google LLC, written by Richard Kralovic <riso@google.com>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/soundcard.h>
+#include <sys/types.h>
+#include <sys/un.h>
+
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <fftw3.h>
+#include <getopt.h>
+#include <math.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysexits.h>
+#include <unistd.h>
+
+#include "virtual_oss.h"
+
+struct Equalizer {
+ double rate;
+ int block_size;
+ int do_normalize;
+
+ /* (block_size * 2) elements, time domain */
+ double *fftw_time;
+
+ /* (block_size * 2) elements, half-complex, freq domain */
+ double *fftw_freq;
+
+ fftw_plan forward;
+ fftw_plan inverse;
+};
+
+static int be_silent = 0;
+
+static void
+message(const char *fmt,...)
+{
+ va_list list;
+
+ if (be_silent)
+ return;
+ va_start(list, fmt);
+ vfprintf(stderr, fmt, list);
+ va_end(list);
+}
+
+/*
+ * Masking window value for -1 < x < 1.
+ *
+ * Window must be symmetric, thus, this function is queried for x >= 0
+ * only. Currently a Hann window.
+ */
+static double
+equalizer_get_window(double x)
+{
+ return (0.5 + 0.5 * cos(M_PI * x));
+}
+
+static int
+equalizer_load_freq_amps(struct Equalizer *e, const char *config)
+{
+ double prev_f = 0.0;
+ double prev_amp = 1.0;
+ double next_f = 0.0;
+ double next_amp = 1.0;
+ int i;
+
+ if (strncasecmp(config, "normalize", 4) == 0) {
+ while (*config != 0) {
+ if (*config == '\n') {
+ config++;
+ break;
+ }
+ config++;
+ }
+ e->do_normalize = 1;
+ } else {
+ e->do_normalize = 0;
+ }
+
+ for (i = 0; i <= (e->block_size / 2); ++i) {
+ const double f = (i * e->rate) / e->block_size;
+
+ while (f >= next_f) {
+ prev_f = next_f;
+ prev_amp = next_amp;
+
+ if (*config == 0) {
+ next_f = e->rate;
+ next_amp = prev_amp;
+ } else {
+ int len;
+
+ if (sscanf(config, "%lf %lf %n", &next_f, &next_amp, &len) == 2) {
+ config += len;
+ if (next_f < prev_f) {
+ message("Parse error: Nonincreasing sequence of frequencies.\n");
+ return (0);
+ }
+ } else {
+ message("Parse error.\n");
+ return (0);
+ }
+ }
+ if (prev_f == 0.0)
+ prev_amp = next_amp;
+ }
+ e->fftw_freq[i] = ((f - prev_f) / (next_f - prev_f)) * (next_amp - prev_amp) + prev_amp;
+ }
+ return (1);
+}
+
+static void
+equalizer_init(struct Equalizer *e, int rate, int block_size)
+{
+ size_t buffer_size;
+
+ e->rate = rate;
+ e->block_size = block_size;
+
+ buffer_size = sizeof(double) * e->block_size;
+
+ e->fftw_time = (double *)malloc(buffer_size);
+ e->fftw_freq = (double *)malloc(buffer_size);
+
+ e->forward = fftw_plan_r2r_1d(block_size, e->fftw_time, e->fftw_freq,
+ FFTW_R2HC, FFTW_MEASURE);
+ e->inverse = fftw_plan_r2r_1d(block_size, e->fftw_freq, e->fftw_time,
+ FFTW_HC2R, FFTW_MEASURE);
+}
+
+static int
+equalizer_load(struct Equalizer *eq, const char *config)
+{
+ int retval = 0;
+ int N = eq->block_size;
+ int buffer_size = sizeof(double) * N;
+ int i;
+
+ memset(eq->fftw_freq, 0, buffer_size);
+
+ message("\n\nReloading amplification specifications:\n%s\n", config);
+
+ if (!equalizer_load_freq_amps(eq, config))
+ goto end;
+
+ double *requested_freq = (double *)malloc(buffer_size);
+
+ memcpy(requested_freq, eq->fftw_freq, buffer_size);
+
+ fftw_execute(eq->inverse);
+
+ /* Multiply by symmetric window and shift */
+ for (i = 0; i < (N / 2); ++i) {
+ double weight = equalizer_get_window(i / (double)(N / 2)) / N;
+
+ eq->fftw_time[N / 2 + i] = eq->fftw_time[i] * weight;
+ }
+ for (i = (N / 2 - 1); i > 0; --i) {
+ eq->fftw_time[i] = eq->fftw_time[N - i];
+ }
+ eq->fftw_time[0] = 0;
+
+ fftw_execute(eq->forward);
+ for (i = 0; i < N; ++i) {
+ eq->fftw_freq[i] /= (double)N;
+ }
+
+ /* Debug output */
+ for (i = 0; i <= (N / 2); ++i) {
+ double f = (eq->rate / N) * i;
+ double a = sqrt(pow(eq->fftw_freq[i], 2.0) +
+ ((i > 0 && i < N / 2) ? pow(eq->fftw_freq[N - i], 2.0) : 0));
+
+ a *= N;
+ double r = requested_freq[i];
+
+ message("%3.1lf Hz: requested %2.2lf, got %2.7lf (log10 = %.2lf), %3.7lfdb\n",
+ f, r, a, log(a) / log(10), (log(a / r) / log(10.0)) * 10.0);
+ }
+
+ /* Normalize FIR filter, if any */
+ if (eq->do_normalize) {
+ double sum = 0;
+
+ for (i = 0; i < N; ++i)
+ sum += fabs(eq->fftw_time[i]);
+ if (sum != 0.0) {
+ for (i = 0; i < N; ++i)
+ eq->fftw_time[i] /= sum;
+ }
+ }
+ for (i = 0; i < N; ++i) {
+ message("%.3lf ms: %.10lf\n", 1000.0 * i / eq->rate, eq->fftw_time[i]);
+ }
+
+ /* End of debug */
+
+ retval = 1;
+
+ free(requested_freq);
+end:
+ return (retval);
+}
+
+static void
+equalizer_done(struct Equalizer *eq)
+{
+
+ fftw_destroy_plan(eq->forward);
+ fftw_destroy_plan(eq->inverse);
+ free(eq->fftw_time);
+ free(eq->fftw_freq);
+}
+
+static struct option equalizer_opts[] = {
+ {"device", required_argument, NULL, 'd'},
+ {"part", required_argument, NULL, 'p'},
+ {"channels", required_argument, NULL, 'c'},
+ {"what", required_argument, NULL, 'w'},
+ {"off", no_argument, NULL, 'o'},
+ {"quiet", no_argument, NULL, 'q'},
+ {"file", no_argument, NULL, 'f'},
+ {"help", no_argument, NULL, 'h'},
+};
+
+static void
+usage(void)
+{
+ message("Usage: virtual_equalizer -d /dev/vdsp.ctl \n"
+ "\t -d, --device [control device]\n"
+ "\t -w, --what [rx_dev,tx_dev,rx_loop,tx_loop, default tx_dev]\n"
+ "\t -p, --part [part number, default 0]\n"
+ "\t -c, --channels [channels, default -1]\n"
+ "\t -f, --file [read input from file, default standard input]\n"
+ "\t -o, --off [disable equalizer]\n"
+ "\t -q, --quiet\n"
+ "\t -h, --help\n");
+ exit(EX_USAGE);
+}
+
+int
+main(int argc, char **argv)
+{
+ struct virtual_oss_fir_filter fir = {};
+ struct virtual_oss_io_info info = {};
+
+ struct Equalizer e;
+
+ char buffer[65536];
+ unsigned cmd_fir_set = VIRTUAL_OSS_SET_TX_DEV_FIR_FILTER;
+ unsigned cmd_fir_get = VIRTUAL_OSS_GET_TX_DEV_FIR_FILTER;
+ unsigned cmd_info = VIRTUAL_OSS_GET_DEV_INFO;
+ const char *dsp = NULL;
+ int rate;
+ int channels = -1;
+ int part = 0;
+ int opt;
+ int len;
+ int offset;
+ int disable = 0;
+ int f = STDIN_FILENO;
+
+ while ((opt = getopt_long(argc, argv, "d:c:f:op:w:qh",
+ equalizer_opts, NULL)) != -1) {
+ switch (opt) {
+ case 'd':
+ dsp = optarg;
+ break;
+ case 'c':
+ channels = atoi(optarg);
+ if (channels == 0) {
+ message("Wrong number of channels\n");
+ usage();
+ }
+ break;
+ case 'p':
+ part = atoi(optarg);
+ if (part < 0) {
+ message("Invalid part number\n");
+ usage();
+ }
+ break;
+ case 'w':
+ if (strcmp(optarg, "rx_dev") == 0) {
+ cmd_fir_set = VIRTUAL_OSS_SET_RX_DEV_FIR_FILTER;
+ cmd_fir_get = VIRTUAL_OSS_GET_RX_DEV_FIR_FILTER;
+ cmd_info = VIRTUAL_OSS_GET_DEV_INFO;
+ } else if (strcmp(optarg, "tx_dev") == 0) {
+ cmd_fir_set = VIRTUAL_OSS_SET_TX_DEV_FIR_FILTER;
+ cmd_fir_get = VIRTUAL_OSS_GET_TX_DEV_FIR_FILTER;
+ cmd_info = VIRTUAL_OSS_GET_DEV_INFO;
+ } else if (strcmp(optarg, "rx_loop") == 0) {
+ cmd_fir_set = VIRTUAL_OSS_SET_RX_LOOP_FIR_FILTER;
+ cmd_fir_get = VIRTUAL_OSS_GET_RX_LOOP_FIR_FILTER;
+ cmd_info = VIRTUAL_OSS_GET_LOOP_INFO;
+ } else if (strcmp(optarg, "tx_loop") == 0) {
+ cmd_fir_set = VIRTUAL_OSS_SET_TX_LOOP_FIR_FILTER;
+ cmd_fir_get = VIRTUAL_OSS_GET_TX_LOOP_FIR_FILTER;
+ cmd_info = VIRTUAL_OSS_GET_LOOP_INFO;
+ } else {
+ message("Bad -w argument not recognized\n");
+ usage();
+ }
+ break;
+ case 'f':
+ if (f != STDIN_FILENO) {
+ message("Can only specify one file\n");
+ usage();
+ }
+ f = open(optarg, O_RDONLY);
+ if (f < 0) {
+ message("Cannot open specified file\n");
+ usage();
+ }
+ break;
+ case 'o':
+ disable = 1;
+ break;
+ case 'q':
+ be_silent = 1;
+ break;
+ default:
+ usage();
+ }
+ }
+
+ fir.number = part;
+ info.number = part;
+
+ int fd = open(dsp, O_RDWR);
+
+ if (fd < 0) {
+ message("Cannot open DSP device\n");
+ return (EX_SOFTWARE);
+ }
+ if (ioctl(fd, VIRTUAL_OSS_GET_SAMPLE_RATE, &rate) < 0) {
+ message("Cannot get sample rate\n");
+ return (EX_SOFTWARE);
+ }
+ if (ioctl(fd, cmd_fir_get, &fir) < 0) {
+ message("Cannot get current FIR filter\n");
+ return (EX_SOFTWARE);
+ }
+ if (disable) {
+ for (fir.channel = 0; fir.channel != channels; fir.channel++) {
+ if (ioctl(fd, cmd_fir_set, &fir) < 0) {
+ if (fir.channel == 0) {
+ message("Cannot disable FIR filter\n");
+ return (EX_SOFTWARE);
+ }
+ break;
+ }
+ }
+ return (0);
+ }
+ equalizer_init(&e, rate, fir.filter_size);
+ equalizer_load(&e, "");
+
+ if (f == STDIN_FILENO) {
+ if (ioctl(fd, cmd_info, &info) < 0) {
+ message("Cannot read part information\n");
+ return (EX_SOFTWARE);
+ }
+ message("Please enter EQ layout for %s, <freq> <gain>:\n", info.name);
+ }
+ offset = 0;
+ while (1) {
+ if (offset == (int)(sizeof(buffer) - 1)) {
+ message("Too much input data\n");
+ return (EX_SOFTWARE);
+ }
+ len = read(f, buffer + offset, sizeof(buffer) - 1 - offset);
+ if (len <= 0)
+ break;
+ offset += len;
+ }
+ buffer[offset] = 0;
+ close(f);
+
+ if (f == STDIN_FILENO)
+ message("Loading new EQ layout\n");
+
+ if (equalizer_load(&e, buffer) == 0) {
+ message("Invalid equalizer data\n");
+ return (EX_SOFTWARE);
+ }
+ fir.filter_data = e.fftw_time;
+
+ for (fir.channel = 0; fir.channel != channels; fir.channel++) {
+ if (ioctl(fd, cmd_fir_set, &fir) < 0) {
+ if (fir.channel == 0)
+ message("Cannot set FIR filter on channel\n");
+ break;
+ }
+ }
+
+ close(fd);
+ equalizer_done(&e);
+
+ return (0);
+}
diff --git a/usr.sbin/virtual_oss/virtual_equalizer/virtual_equalizer.8 b/usr.sbin/virtual_oss/virtual_equalizer/virtual_equalizer.8
new file mode 100644
index 000000000000..db47db84305e
--- /dev/null
+++ b/usr.sbin/virtual_oss/virtual_equalizer/virtual_equalizer.8
@@ -0,0 +1,127 @@
+.\"
+.\" Copyright (c) 2019 Google LLC, written by Richard Kralovic <riso@google.com>
+.\"
+.\" All rights reserved.
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions
+.\" are met:
+.\" 1. Redistributions of source code must retain the above copyright
+.\" notice, this list of conditions and the following disclaimer.
+.\" 2. Redistributions in binary form must reproduce the above copyright
+.\" notice, this list of conditions and the following disclaimer in the
+.\" documentation and/or other materials provided with the distribution.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+.\" SUCH DAMAGE.
+.\"
+.\"
+.Dd February 12, 2025
+.Dt VIRTUAL_EQUALIZER 8
+.Os
+.Sh NAME
+.Nm virtual_equalizer
+.Nd audio equalizer
+.Sh SYNOPSIS
+.Nm
+.Op Fl h
+.Op Fl o
+.Op Fl q
+.Op Fl d Ar devname
+.Op Fl w Ar what
+.Op Fl p Ar part
+.Op Fl c Ar channels
+.Op Fl f Ar file
+.Sh DESCRIPTION
+.Nm
+sets the given frequency response for the given
+.Xr virtual_oss 8
+instance via the control character device given by the -d option.
+The design goal of this equalizer is to provide precise equalization
+for arbitrary requested frequency response at the expense of higher
+latency, utilizing a so-called finite impulse response, FIR, filter.
+.Pp
+The requested frequency response is configured via standard input or
+the file specified by the -f option.
+There is one control point in per line.
+Each line consists of two numbers, frequency in Hz and requested
+amplification.
+Amplification between two consecutive control points is a linear
+interpolation of the given control point values.
+.Pp
+To make the filter finite, it is windowed in time domain using a Hann
+window.
+The windowing actually modifies the frequency response - the actual
+response is a convolution of the requested response and spectrum of
+the window.
+This is, however, very close to the requested response.
+.Pp
+The following options are available:
+.Bl -tag -width indent
+.It Fl q
+Be quiet and don't print anything to standard output.
+.It Fl d Ar device
+The
+.Xr virtual_oss 8
+control character device.
+.It Fl w Ar what
+Select what part the FIR filter should apply to.
+Valid values are: rx_dev, tx_dev, rx_loop and tx_loop.
+The default value is tx_dev.
+.It Fl p Ar part
+Select the index of the part given by the -w option to apply the filter to.
+Default is zero.
+.It Fl c Ar channels
+Select number of channels to apply filter to, starting at channel zero.
+By default all channels of the given part are updated.
+.It Fl f Ar file
+Read filter coefficients from the given file instead of standard input.
+.It Fl o
+Turn equalizer off.
+.It Fl h
+Show usage.
+.El
+.Sh EXAMPLES
+To pass only frequencies between 200Hz and 400Hz:
+.Bd -literal -offset indent
+# Note that the -F and -G options enable FIR filtering.
+virtual_oss -B -C 2 -c 2 -S -Q 0 -b 32 -r 48000 -s 8ms -F 80ms -G 80ms \\
+ -f /dev/dsp -d dsp.virtual -t vdsp.ctl
+
+# For simplex operation use this:
+virtual_oss -B -C 2 -c 2 -S -Q 0 -b 32 -r 48000 -s 8ms -F 80ms -G 80ms \\
+ -R /dev/null -O /dev/dsp -d dsp.virtual -t vdsp.ctl
+
+# Load normalized filter points to avoid sample value overflow
+cat << EOF | virtual_equalizer -d /dev/vdsp.ctl -w tx_dev -p 0 -c 2
+NORMALIZE
+199 0.0
+200 1.0
+400 1.0
+401 0.0
+EOF
+
+# Load FIR filter based on sine frequency points
+cat << EOF | virtual_equalizer -d /dev/vdsp.ctl -w tx_dev -p 0 -c 2
+199 0.0
+200 1.0
+400 1.0
+401 0.0
+EOF
+
+.Ed
+.Sh SEE ALSO
+.Xr virtual_oss 8
+.Sh AUTHORS
+.Nm
+was written by
+.An Richard Kralovic riso@google.com .