yukawa: Add loudspeaker equalization
This filter attenuates 200-400Hz by 18dB,
and some 6dB notch attenuation at 2.25kHz, 3.8kHz, 6.6kHz.
Full frequency response here: https://b.corp.google.com/issues/159714063#comment3
EQ can be disabled/modify at runtime with the following steps:
1. (Disable) Rename file /vendor/etc/speaker_eq_sei610.fir to a different name/ext.
(Modify) Modify contents of /vendor/etc/speaker_eq_sei610.fir (max 512 taps).
2. Run 'killall audioserver' on device.
Bug: b/159714063
Test: Yukawa passed ART Eraser test.
Change-Id: I32ccb20c9fd1d6ff51086d93babbd9cf828edc0d
diff --git a/device-yukawa.mk b/device-yukawa.mk
index ed817a5..c2edd61 100644
--- a/device-yukawa.mk
+++ b/device-yukawa.mk
@@ -22,3 +22,7 @@
# Feature permissions
PRODUCT_COPY_FILES += \
device/amlogic/yukawa/permissions/yukawa.xml:/system/etc/sysconfig/yukawa.xml
+
+# Speaker EQ
+PRODUCT_COPY_FILES += \
+ device/amlogic/yukawa/hal/audio/speaker_eq_sei610.fir:$(TARGET_COPY_OUT_VENDOR)/etc/speaker_eq_sei610.fir
diff --git a/hal/audio/Android.mk b/hal/audio/Android.mk
index 357f12b..67a248d 100644
--- a/hal/audio/Android.mk
+++ b/hal/audio/Android.mk
@@ -29,7 +29,8 @@
LOCAL_SRC_FILES := audio_hw.c \
audio_aec.c \
- fifo_wrapper.cpp
+ fifo_wrapper.cpp \
+ fir_filter.c
LOCAL_SHARED_LIBRARIES := liblog libcutils libtinyalsa libaudioroute libaudioutils
LOCAL_CFLAGS := -Wno-unused-parameter
LOCAL_C_INCLUDES += \
diff --git a/hal/audio/audio_hw.c b/hal/audio/audio_hw.c
index d145b42..bbfb608 100644
--- a/hal/audio/audio_hw.c
+++ b/hal/audio/audio_hw.c
@@ -46,13 +46,14 @@
#include <sys/ioctl.h>
-#include "audio_hw.h"
#include "audio_aec.h"
+#include "audio_hw.h"
static int adev_get_mic_mute(const struct audio_hw_device* dev, bool* state);
static int adev_get_microphones(const struct audio_hw_device* dev,
struct audio_microphone_characteristic_t* mic_array,
size_t* mic_count);
+static size_t out_get_buffer_size(const struct audio_stream* stream);
static int get_audio_output_port(audio_devices_t devices) {
/* Prefer HDMI, default to internal speaker */
@@ -101,6 +102,56 @@
return ret;
}
+static int read_filter_from_file(const char* filename, int16_t* filter, int max_length) {
+ FILE* fp = fopen(filename, "r");
+ if (fp == NULL) {
+ ALOGI("%s: File %s not found.", __func__, filename);
+ return 0;
+ }
+ int num_taps = 0;
+ char* line = NULL;
+ size_t len = 0;
+ while (!feof(fp)) {
+ size_t size = getline(&line, &len, fp);
+ if ((line[0] == '#') || (size < 2)) {
+ continue;
+ }
+ int n = sscanf(line, "%" PRIi16 "\n", &filter[num_taps++]);
+ if (n < 1) {
+ ALOGE("Could not find coefficient %d! Exiting...", num_taps - 1);
+ return 0;
+ }
+ ALOGV("Coeff %d : %" PRIi16, num_taps, filter[num_taps - 1]);
+ if (num_taps == max_length) {
+ ALOGI("%s: max tap length %d reached.", __func__, max_length);
+ break;
+ }
+ }
+ free(line);
+ fclose(fp);
+ return num_taps;
+}
+
+static void out_set_eq(struct alsa_stream_out* out) {
+ out->speaker_eq = NULL;
+ int16_t* speaker_eq_coeffs = (int16_t*)calloc(SPEAKER_MAX_EQ_LENGTH, sizeof(int16_t));
+ if (speaker_eq_coeffs == NULL) {
+ ALOGE("%s: Failed to allocate speaker EQ", __func__);
+ return;
+ }
+ int num_taps = read_filter_from_file(SPEAKER_EQ_FILE, speaker_eq_coeffs, SPEAKER_MAX_EQ_LENGTH);
+ if (num_taps == 0) {
+ ALOGI("%s: Empty filter file or 0 taps set.", __func__);
+ free(speaker_eq_coeffs);
+ return;
+ }
+ out->speaker_eq = fir_init(
+ out->config.channels, FIR_SINGLE_FILTER, num_taps,
+ out_get_buffer_size(&out->stream.common) / out->config.channels / sizeof(int16_t),
+ speaker_eq_coeffs);
+ free(speaker_eq_coeffs);
+}
+
/* must be called with hw device and output stream mutexes locked */
static int start_output_stream(struct alsa_stream_out *out)
{
@@ -185,6 +236,8 @@
{
struct alsa_audio_device *adev = out->dev;
+ fir_reset(out->speaker_eq);
+
if (!out->standby) {
pcm_close(out->pcm);
out->pcm = NULL;
@@ -292,6 +345,9 @@
pthread_mutex_unlock(&adev->lock);
+ if (out->speaker_eq != NULL) {
+ fir_process_interleaved(out->speaker_eq, (int16_t*)buffer, (int16_t*)buffer, out_frames);
+ }
ret = pcm_write(out->pcm, buffer, out_frames * frame_size);
if (ret == 0) {
@@ -793,6 +849,14 @@
*stream_out = &out->stream;
+ out->speaker_eq = NULL;
+ if (out_port == PORT_INTERNAL_SPEAKER) {
+ out_set_eq(out);
+ if (out->speaker_eq == NULL) {
+ ALOGE("%s: Failed to initialize speaker EQ", __func__);
+ }
+ }
+
/* TODO The retry mechanism isn't implemented in AudioPolicyManager/AudioFlinger. */
ret = 0;
@@ -813,6 +877,8 @@
ALOGV("adev_close_output_stream...");
struct alsa_audio_device *adev = (struct alsa_audio_device *)dev;
destroy_aec_reference_config(adev->aec);
+ struct alsa_stream_out* out = (struct alsa_stream_out*)stream;
+ fir_release(out->speaker_eq);
free(stream);
}
diff --git a/hal/audio/audio_hw.h b/hal/audio/audio_hw.h
index 5dd4f46..129bdbf 100644
--- a/hal/audio/audio_hw.h
+++ b/hal/audio/audio_hw.h
@@ -20,6 +20,8 @@
#include <hardware/audio.h>
#include <tinyalsa/asoundlib.h>
+#include "fir_filter.h"
+
#define CARD_OUT 0
#define PORT_HDMI 0
#define PORT_INTERNAL_SPEAKER 1
@@ -65,6 +67,9 @@
#define PLAYBACK_CODEC_SAMPLING_RATE 48000
#define MIN_WRITE_SLEEP_US 5000
+#define SPEAKER_EQ_FILE "/vendor/etc/speaker_eq_sei610.fir"
+#define SPEAKER_MAX_EQ_LENGTH 512
+
struct alsa_audio_device {
struct audio_hw_device hw_device;
@@ -106,6 +111,7 @@
int write_threshold;
unsigned int frames_written;
struct timespec timestamp;
+ fir_filter_t* speaker_eq;
};
/* 'bytes' are the number of bytes written to audio FIFO, for which 'timestamp' is valid.
diff --git a/hal/audio/fir_filter.c b/hal/audio/fir_filter.c
new file mode 100644
index 0000000..c648fc0
--- /dev/null
+++ b/hal/audio/fir_filter.c
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "audio_hw_fir_filter"
+//#define LOG_NDEBUG 0
+
+#include <assert.h>
+#include <audio_utils/primitives.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <log/log.h>
+#include <malloc.h>
+#include <string.h>
+
+#include "fir_filter.h"
+
+#ifdef __ARM_NEON
+#include "arm_neon.h"
+#endif /* #ifdef __ARM_NEON */
+
+fir_filter_t* fir_init(uint32_t channels, fir_filter_mode_t mode, uint32_t filter_length,
+ uint32_t input_length, int16_t* coeffs) {
+ if ((channels == 0) || (filter_length == 0) || (coeffs == NULL)) {
+ ALOGE("%s: Invalid channel count, filter length or coefficient array.", __func__);
+ return NULL;
+ }
+
+ fir_filter_t* fir = (fir_filter_t*)calloc(1, sizeof(fir_filter_t));
+ if (fir == NULL) {
+ ALOGE("%s: Unable to allocate memory for fir_filter.", __func__);
+ return NULL;
+ }
+
+ fir->channels = channels;
+ fir->filter_length = filter_length;
+ /* Default: same filter coeffs for all channels */
+ fir->mode = FIR_SINGLE_FILTER;
+ uint32_t coeff_bytes = fir->filter_length * sizeof(int16_t);
+ if (mode == FIR_PER_CHANNEL_FILTER) {
+ fir->mode = FIR_PER_CHANNEL_FILTER;
+ coeff_bytes = fir->filter_length * fir->channels * sizeof(int16_t);
+ }
+
+ fir->coeffs = (int16_t*)malloc(coeff_bytes);
+ if (fir->coeffs == NULL) {
+ ALOGE("%s: Unable to allocate memory for FIR coeffs", __func__);
+ goto exit_1;
+ }
+ memcpy(fir->coeffs, coeffs, coeff_bytes);
+
+ fir->buffer_size = (input_length + fir->filter_length) * fir->channels;
+ fir->state = (int16_t*)malloc(fir->buffer_size * sizeof(int16_t));
+ if (fir->state == NULL) {
+ ALOGE("%s: Unable to allocate memory for FIR state", __func__);
+ goto exit_2;
+ }
+
+#ifdef __ARM_NEON
+ ALOGI("%s: Using ARM Neon", __func__);
+#endif /* #ifdef __ARM_NEON */
+
+ fir_reset(fir);
+ return fir;
+
+exit_2:
+ free(fir->coeffs);
+exit_1:
+ free(fir);
+ return NULL;
+}
+
+void fir_release(fir_filter_t* fir) {
+ if (fir == NULL) {
+ return;
+ }
+ free(fir->state);
+ free(fir->coeffs);
+ free(fir);
+}
+
+void fir_reset(fir_filter_t* fir) {
+ if (fir == NULL) {
+ return;
+ }
+ memset(fir->state, 0, fir->buffer_size * sizeof(int16_t));
+}
+
+void fir_process_interleaved(fir_filter_t* fir, int16_t* input, int16_t* output, uint32_t samples) {
+ assert(fir != NULL);
+
+ int start_offset = (fir->filter_length - 1) * fir->channels;
+ memcpy(&fir->state[start_offset], input, samples * fir->channels * sizeof(int16_t));
+ // int ch;
+ bool use_2nd_set_coeffs = (fir->channels > 1) && (fir->mode == FIR_PER_CHANNEL_FILTER);
+ int16_t* p_coeff_A = &fir->coeffs[0];
+ int16_t* p_coeff_B = use_2nd_set_coeffs ? &fir->coeffs[fir->filter_length] : &fir->coeffs[0];
+ int16_t* p_output;
+ for (int ch = 0; ch < fir->channels; ch += 2) {
+ p_output = &output[ch];
+ int offset = start_offset + ch;
+ for (int s = 0; s < samples; s++) {
+ int32_t acc_A = 0;
+ int32_t acc_B = 0;
+
+#ifdef __ARM_NEON
+ int32x4_t acc_vec = vdupq_n_s32(0);
+ for (int k = 0; k < fir->filter_length; k++, offset -= fir->channels) {
+ int16x4_t coeff_vec = vdup_n_s16(p_coeff_A[k]);
+ coeff_vec = vset_lane_s16(p_coeff_B[k], coeff_vec, 1);
+ int16x4_t input_vec = vld1_s16(&fir->state[offset]);
+ acc_vec = vmlal_s16(acc_vec, coeff_vec, input_vec);
+ }
+ acc_A = vgetq_lane_s32(acc_vec, 0);
+ acc_B = vgetq_lane_s32(acc_vec, 1);
+#else
+ for (int k = 0; k < fir->filter_length; k++, offset -= fir->channels) {
+ int32_t input_A = (int32_t)(fir->state[offset]);
+ int32_t coeff_A = (int32_t)(p_coeff_A[k]);
+ int32_t input_B = (int32_t)(fir->state[offset + 1]);
+ int32_t coeff_B = (int32_t)(p_coeff_B[k]);
+ acc_A += (input_A * coeff_A);
+ acc_B += (input_B * coeff_B);
+ }
+#endif /* #ifdef __ARM_NEON */
+
+ *p_output = clamp16(acc_A >> 15);
+ if (ch < fir->channels - 1) {
+ *(p_output + 1) = clamp16(acc_B >> 15);
+ }
+ /* Move to next sample */
+ p_output += fir->channels;
+ offset += (fir->filter_length + 1) * fir->channels;
+ }
+ if (use_2nd_set_coeffs) {
+ p_coeff_A += (fir->filter_length << 1);
+ p_coeff_B += (fir->filter_length << 1);
+ }
+ }
+ memmove(fir->state, &fir->state[samples * fir->channels],
+ (fir->filter_length - 1) * fir->channels * sizeof(int16_t));
+}
diff --git a/hal/audio/fir_filter.h b/hal/audio/fir_filter.h
new file mode 100644
index 0000000..d8c6e91
--- /dev/null
+++ b/hal/audio/fir_filter.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FIR_FILTER_H
+#define FIR_FILTER_H
+
+#include <stdint.h>
+
+typedef enum fir_filter_mode { FIR_SINGLE_FILTER = 0, FIR_PER_CHANNEL_FILTER } fir_filter_mode_t;
+
+typedef struct fir_filter {
+ fir_filter_mode_t mode;
+ uint32_t channels;
+ uint32_t filter_length;
+ uint32_t buffer_size;
+ int16_t* coeffs;
+ int16_t* state;
+} fir_filter_t;
+
+fir_filter_t* fir_init(uint32_t channels, fir_filter_mode_t mode, uint32_t filter_length,
+ uint32_t input_length, int16_t* coeffs);
+void fir_release(fir_filter_t* fir);
+void fir_reset(fir_filter_t* fir);
+void fir_process_interleaved(fir_filter_t* fir, int16_t* input, int16_t* output, uint32_t samples);
+
+#endif /* #ifndef FIR_FILTER_H */
diff --git a/hal/audio/speaker_eq_sei610.fir b/hal/audio/speaker_eq_sei610.fir
new file mode 100644
index 0000000..2352c32
--- /dev/null
+++ b/hal/audio/speaker_eq_sei610.fir
@@ -0,0 +1,523 @@
+# FIR speaker EQ file for SEI-610
+# This filter attenuates 200-400Hz by 18dB,
+# and some 6dB notch attenuation at 2.25kHz, 3.8kHz, 6.6kHz.
+# Script to generate this file: https://drive.google.com/file/d/1_qvkZ8nU-c6tD6XrH80et2P12paardAz/view?usp=sharing
+
+# Full frequency response here: https://b.corp.google.com/issues/159714063#comment3
+
+# Each FIR coefficient is specified on one line (no leading spaces).
+# First line is 0th coefficient.
+# Values must be 16-bit integers. Currently, a max of 512 taps is supported.
+
+18976
+9870
+-12520
+2452
+-766
+-1023
+1122
+-2509
+316
+-1464
+95
+-817
+-1191
+-1882
+-2299
+-1806
+-1180
+-310
+-68
+-303
+-957
+-1544
+-1738
+-1490
+-973
+-517
+-285
+-261
+-247
+-68
+305
+729
+983
+931
+612
+210
+-63
+-100
+48
+234
+313
+244
+99
+3
+36
+183
+350
+435
+398
+286
+191
+188
+282
+409
+483
+454
+336
+192
+92
+63
+83
+100
+73
+2
+-75
+-114
+-93
+-27
+41
+73
+55
+9
+-30
+-38
+-14
+18
+30
+9
+-34
+-78
+-100
+-94
+-75
+-62
+-68
+-91
+-116
+-124
+-109
+-79
+-50
+-35
+-37
+-47
+-53
+-48
+-33
+-19
+-14
+-22
+-38
+-51
+-55
+-49
+-38
+-31
+-32
+-38
+-45
+-45
+-38
+-26
+-17
+-14
+-16
+-21
+-23
+-21
+-16
+-12
+-13
+-17
+-25
+-30
+-32
+-31
+-29
+-28
+-30
+-33
+-36
+-37
+-35
+-32
+-30
+-31
+-33
+-36
+-38
+-38
+-37
+-37
+-38
+-40
+-43
+-46
+-47
+-47
+-46
+-46
+-47
+-49
+-50
+-50
+-50
+-49
+-48
+-49
+-50
+-51
+-51
+-51
+-51
+-51
+-51
+-52
+-53
+-54
+-54
+-54
+-54
+-54
+-54
+-55
+-55
+-55
+-54
+-54
+-54
+-54
+-54
+-55
+-55
+-55
+-55
+-55
+-55
+-55
+-56
+-56
+-56
+-56
+-56
+-56
+-56
+-56
+-56
+-56
+-56
+-55
+-55
+-55
+-56
+-56
+-56
+-56
+-55
+-55
+-55
+-56
+-56
+-56
+-55
+-55
+-55
+-55
+-55
+-55
+-55
+-55
+-55
+-54
+-54
+-54
+-54
+-54
+-54
+-54
+-53
+-53
+-53
+-53
+-53
+-53
+-52
+-52
+-52
+-52
+-51
+-51
+-51
+-51
+-50
+-50
+-50
+-50
+-49
+-49
+-49
+-48
+-48
+-48
+-48
+-47
+-47
+-47
+-46
+-46
+-46
+-45
+-45
+-45
+-44
+-44
+-44
+-43
+-43
+-43
+-42
+-42
+-41
+-41
+-41
+-40
+-40
+-40
+-39
+-39
+-38
+-38
+-38
+-37
+-37
+-36
+-36
+-36
+-35
+-35
+-34
+-34
+-33
+-33
+-33
+-32
+-32
+-31
+-31
+-31
+-30
+-30
+-29
+-29
+-28
+-28
+-27
+-27
+-27
+-26
+-26
+-25
+-25
+-24
+-24
+-24
+-23
+-23
+-22
+-22
+-21
+-21
+-20
+-20
+-20
+-19
+-19
+-18
+-18
+-17
+-17
+-17
+-16
+-16
+-15
+-15
+-14
+-14
+-14
+-13
+-13
+-12
+-12
+-11
+-11
+-11
+-10
+-10
+-9
+-9
+-9
+-8
+-8
+-7
+-7
+-7
+-6
+-6
+-5
+-5
+-5
+-4
+-4
+-3
+-3
+-3
+-2
+-2
+-1
+-1
+-1
+0
+0
+0
+0
+0
+0
+1
+1
+1
+2
+2
+2
+3
+3
+3
+4
+4
+4
+5
+5
+5
+6
+6
+6
+7
+7
+7
+7
+8
+8
+8
+9
+9
+9
+9
+10
+10
+10
+10
+11
+11
+11
+11
+12
+12
+12
+12
+13
+13
+13
+13
+13
+14
+14
+14
+14
+14
+15
+15
+15
+15
+15
+16
+16
+16
+16
+16
+16
+17
+17
+17
+17
+17
+17
+17
+18
+18
+18
+18
+18
+18
+18
+18
+19
+19
+19
+19
+19
+19
+19
+19
+19
+19
+19
+20
+20
+20
+20
+20
+20
+20
+20
+20
+20
+20
+20
+20
+20
+20
+20
+20
+20
+21
+21
+21
+21
+21
+21
+21
+21
+21
+21
+21
+21
+21
+21
+21
+21
+21
+21
+21
+21
+21
+21
+21
+21
+21
+20
+20
+20
+20
+20
+20